smartbroker-plus-sdk 0.3.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.
- smartbroker_plus_sdk/__init__.py +242 -0
- smartbroker_plus_sdk/__main__.py +9 -0
- smartbroker_plus_sdk/_auth/__init__.py +0 -0
- smartbroker_plus_sdk/_auth/_account_prefs/__init__.py +8 -0
- smartbroker_plus_sdk/_auth/_account_prefs/_account_preferences.py +68 -0
- smartbroker_plus_sdk/_auth/_account_prefs/_account_prefs_store.py +133 -0
- smartbroker_plus_sdk/_auth/_active_account/__init__.py +0 -0
- smartbroker_plus_sdk/_auth/_active_account/_active_account.py +22 -0
- smartbroker_plus_sdk/_auth/_active_account/_active_account_store.py +68 -0
- smartbroker_plus_sdk/_auth/_environments.py +91 -0
- smartbroker_plus_sdk/_auth/_oauth.py +505 -0
- smartbroker_plus_sdk/_auth/_token_cache/__init__.py +0 -0
- smartbroker_plus_sdk/_auth/_token_cache/_cached_token.py +103 -0
- smartbroker_plus_sdk/_auth/_token_cache/_file_token_cache.py +155 -0
- smartbroker_plus_sdk/_auth/_token_cache/_null_token_cache.py +49 -0
- smartbroker_plus_sdk/_auth/_token_cache/_path_resolver.py +32 -0
- smartbroker_plus_sdk/_auth/_token_cache/_token_cache.py +48 -0
- smartbroker_plus_sdk/_auth/_two_factor.py +494 -0
- smartbroker_plus_sdk/_auth/_two_factor_poll.py +99 -0
- smartbroker_plus_sdk/_auth/_two_factor_provider.py +52 -0
- smartbroker_plus_sdk/_auth/_two_factor_requests.py +119 -0
- smartbroker_plus_sdk/_cli/__init__.py +0 -0
- smartbroker_plus_sdk/_cli/_account_resolver.py +65 -0
- smartbroker_plus_sdk/_cli/_account_transactions_cmd.py +53 -0
- smartbroker_plus_sdk/_cli/_app.py +153 -0
- smartbroker_plus_sdk/_cli/_auth_cmd.py +325 -0
- smartbroker_plus_sdk/_cli/_client_factory.py +181 -0
- smartbroker_plus_sdk/_cli/_context.py +23 -0
- smartbroker_plus_sdk/_cli/_cost_report_cmd.py +89 -0
- smartbroker_plus_sdk/_cli/_entrypoint.py +42 -0
- smartbroker_plus_sdk/_cli/_environments.py +10 -0
- smartbroker_plus_sdk/_cli/_error_handler.py +184 -0
- smartbroker_plus_sdk/_cli/_health_cmd.py +20 -0
- smartbroker_plus_sdk/_cli/_logging_setup.py +61 -0
- smartbroker_plus_sdk/_cli/_marketstate_cmd.py +44 -0
- smartbroker_plus_sdk/_cli/_modifications_cmd.py +194 -0
- smartbroker_plus_sdk/_cli/_oauth_browser.py +96 -0
- smartbroker_plus_sdk/_cli/_orders_cmd.py +167 -0
- smartbroker_plus_sdk/_cli/_output.py +166 -0
- smartbroker_plus_sdk/_cli/_output_format.py +12 -0
- smartbroker_plus_sdk/_cli/_payload_loader.py +59 -0
- smartbroker_plus_sdk/_cli/_portfolios_cmd.py +77 -0
- smartbroker_plus_sdk/_cli/_positions_cmd.py +127 -0
- smartbroker_plus_sdk/_cli/_profile_cmd.py +40 -0
- smartbroker_plus_sdk/_cli/_prompts.py +32 -0
- smartbroker_plus_sdk/_cli/_tradeability_cmd.py +71 -0
- smartbroker_plus_sdk/_cli/_transactions_cmd.py +77 -0
- smartbroker_plus_sdk/_client.py +505 -0
- smartbroker_plus_sdk/_error_mapping.py +112 -0
- smartbroker_plus_sdk/_exceptions.py +124 -0
- smartbroker_plus_sdk/_models/__init__.py +0 -0
- smartbroker_plus_sdk/_models/_account_transactions.py +42 -0
- smartbroker_plus_sdk/_models/_auth.py +65 -0
- smartbroker_plus_sdk/_models/_common.py +366 -0
- smartbroker_plus_sdk/_models/_cost_report.py +49 -0
- smartbroker_plus_sdk/_models/_marketstate.py +46 -0
- smartbroker_plus_sdk/_models/_modifications.py +162 -0
- smartbroker_plus_sdk/_models/_orders.py +318 -0
- smartbroker_plus_sdk/_models/_portfolios.py +87 -0
- smartbroker_plus_sdk/_models/_positions.py +50 -0
- smartbroker_plus_sdk/_models/_profile.py +13 -0
- smartbroker_plus_sdk/_models/_tradeability.py +79 -0
- smartbroker_plus_sdk/_models/_transactions.py +63 -0
- smartbroker_plus_sdk/_resources/__init__.py +0 -0
- smartbroker_plus_sdk/_resources/_account_transactions.py +96 -0
- smartbroker_plus_sdk/_resources/_account_transactions_requests.py +71 -0
- smartbroker_plus_sdk/_resources/_b2b_base.py +2 -0
- smartbroker_plus_sdk/_resources/_cost_report.py +86 -0
- smartbroker_plus_sdk/_resources/_cost_report_requests.py +31 -0
- smartbroker_plus_sdk/_resources/_health.py +47 -0
- smartbroker_plus_sdk/_resources/_health_requests.py +15 -0
- smartbroker_plus_sdk/_resources/_marketstate.py +70 -0
- smartbroker_plus_sdk/_resources/_marketstate_requests.py +33 -0
- smartbroker_plus_sdk/_resources/_order_modifications.py +260 -0
- smartbroker_plus_sdk/_resources/_order_modifications_requests.py +128 -0
- smartbroker_plus_sdk/_resources/_orders.py +545 -0
- smartbroker_plus_sdk/_resources/_orders_requests.py +272 -0
- smartbroker_plus_sdk/_resources/_portfolios.py +50 -0
- smartbroker_plus_sdk/_resources/_portfolios_requests.py +27 -0
- smartbroker_plus_sdk/_resources/_positions.py +289 -0
- smartbroker_plus_sdk/_resources/_positions_requests.py +89 -0
- smartbroker_plus_sdk/_resources/_profile.py +56 -0
- smartbroker_plus_sdk/_resources/_profile_requests.py +17 -0
- smartbroker_plus_sdk/_resources/_request.py +35 -0
- smartbroker_plus_sdk/_resources/_tradeability.py +122 -0
- smartbroker_plus_sdk/_resources/_tradeability_requests.py +91 -0
- smartbroker_plus_sdk/_resources/_transactions.py +95 -0
- smartbroker_plus_sdk/_resources/_transactions_requests.py +34 -0
- smartbroker_plus_sdk/_resources/_validation.py +57 -0
- smartbroker_plus_sdk/_transport/__init__.py +0 -0
- smartbroker_plus_sdk/_transport/_auth_policy.py +182 -0
- smartbroker_plus_sdk/_transport/_http_transport.py +530 -0
- smartbroker_plus_sdk/_transport/_log_formatting.py +345 -0
- smartbroker_plus_sdk/_transport/_logging_transport.py +83 -0
- smartbroker_plus_sdk/_transport/_logging_transport_async.py +83 -0
- smartbroker_plus_sdk/_transport/_response.py +25 -0
- smartbroker_plus_sdk/_transport/_retry.py +18 -0
- smartbroker_plus_sdk/_transport/_two_factor_guard.py +204 -0
- smartbroker_plus_sdk/_transport/_urls.py +22 -0
- smartbroker_plus_sdk/account/__init__.py +46 -0
- smartbroker_plus_sdk/exceptions.py +43 -0
- smartbroker_plus_sdk/market/__init__.py +29 -0
- smartbroker_plus_sdk/py.typed +0 -0
- smartbroker_plus_sdk/trading/__init__.py +87 -0
- smartbroker_plus_sdk-0.3.0.dist-info/METADATA +250 -0
- smartbroker_plus_sdk-0.3.0.dist-info/RECORD +110 -0
- smartbroker_plus_sdk-0.3.0.dist-info/WHEEL +5 -0
- smartbroker_plus_sdk-0.3.0.dist-info/entry_points.txt +2 -0
- smartbroker_plus_sdk-0.3.0.dist-info/licenses/LICENSE +21 -0
- smartbroker_plus_sdk-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from ._client import AsyncSmartbrokerClient as AsyncSmartbrokerClient
|
|
2
|
+
from ._client import SmartbrokerClient as SmartbrokerClient
|
|
3
|
+
from ._auth._active_account._active_account import ActiveAccount as ActiveAccount
|
|
4
|
+
from ._auth._active_account._active_account_store import (
|
|
5
|
+
ActiveAccountStore as ActiveAccountStore,
|
|
6
|
+
)
|
|
7
|
+
from ._auth._oauth import AsyncOAuthClient as AsyncOAuthClient
|
|
8
|
+
from ._auth._oauth import generate_code_challenge as generate_code_challenge
|
|
9
|
+
from ._auth._oauth import generate_code_verifier as generate_code_verifier
|
|
10
|
+
from ._auth._oauth import OAuthClient as OAuthClient
|
|
11
|
+
from ._auth._token_cache._cached_token import CachedToken as CachedToken
|
|
12
|
+
from ._auth._token_cache._file_token_cache import FileTokenCache as FileTokenCache
|
|
13
|
+
from ._auth._token_cache._path_resolver import resolve_cache_dir as resolve_cache_dir
|
|
14
|
+
from ._exceptions import AuthenticationError as AuthenticationError
|
|
15
|
+
from ._exceptions import AuthorizationError as AuthorizationError
|
|
16
|
+
from ._exceptions import BuyingPowerExceededError as BuyingPowerExceededError
|
|
17
|
+
from ._exceptions import InsufficientBalanceError as InsufficientBalanceError
|
|
18
|
+
from ._exceptions import InvalidLimitStepError as InvalidLimitStepError
|
|
19
|
+
from ._exceptions import (
|
|
20
|
+
MinimumInvestmentNotReachedError as MinimumInvestmentNotReachedError,
|
|
21
|
+
)
|
|
22
|
+
from ._exceptions import NotFoundError as NotFoundError
|
|
23
|
+
from ._exceptions import NumbersNotAllowedError as NumbersNotAllowedError
|
|
24
|
+
from ._exceptions import OrderRejectedError as OrderRejectedError
|
|
25
|
+
from ._exceptions import PennyStockLimitRequiredError as PennyStockLimitRequiredError
|
|
26
|
+
from ._exceptions import RefreshTokenExpiredError as RefreshTokenExpiredError
|
|
27
|
+
from ._exceptions import RetryExhaustedError as RetryExhaustedError
|
|
28
|
+
from ._exceptions import ServerError as ServerError
|
|
29
|
+
from ._exceptions import SmartbrokerApiError as SmartbrokerApiError
|
|
30
|
+
from ._exceptions import StopLimitTooLowError as StopLimitTooLowError
|
|
31
|
+
from ._exceptions import TwoFactorError as TwoFactorError
|
|
32
|
+
from ._exceptions import TwoFactorTokenExpiredError as TwoFactorTokenExpiredError
|
|
33
|
+
from ._exceptions import ValidationError as ValidationError
|
|
34
|
+
from ._models._auth import ApprovalRequiredError as ApprovalRequiredError
|
|
35
|
+
from ._models._auth import Token as Token
|
|
36
|
+
from ._models._auth import TokenResponse as TokenResponse
|
|
37
|
+
from ._models._auth import TokenStatus as TokenStatus
|
|
38
|
+
from ._models._account_transactions import AccountTransactions as AccountTransactions
|
|
39
|
+
from ._models._account_transactions import (
|
|
40
|
+
RestAccountTransaction as RestAccountTransaction,
|
|
41
|
+
)
|
|
42
|
+
from ._models._marketstate import CloseData as CloseData
|
|
43
|
+
from ._models._marketstate import Marketstate as Marketstate
|
|
44
|
+
from ._models._marketstate import QuoteData as QuoteData
|
|
45
|
+
from ._models._common import AssetQuotation as AssetQuotation
|
|
46
|
+
from ._models._common import CostReportPositionType as CostReportPositionType
|
|
47
|
+
from ._models._common import InstrumentType as InstrumentType
|
|
48
|
+
from ._models._common import ModificationRequestStatus as ModificationRequestStatus
|
|
49
|
+
from ._models._common import OrderDirection as OrderDirection
|
|
50
|
+
from ._models._common import OrderDirectionFilter as OrderDirectionFilter
|
|
51
|
+
from ._models._common import OrderModel as OrderModel
|
|
52
|
+
from ._models._common import OrderStateScope as OrderStateScope
|
|
53
|
+
from ._models._common import OrderState as OrderState
|
|
54
|
+
from ._models._common import PaginatedResponse as PaginatedResponse
|
|
55
|
+
from ._models._common import PaginationParams as PaginationParams
|
|
56
|
+
from ._models._common import ProblemDetail as ProblemDetail
|
|
57
|
+
from ._models._common import SortDirection as SortDirection
|
|
58
|
+
from ._models._common import TrailingStopType as TrailingStopType
|
|
59
|
+
from ._models._common import TransactionType as TransactionType
|
|
60
|
+
from ._models._common import ValidityType as ValidityType
|
|
61
|
+
from ._models._cost_report import CostReport as CostReport
|
|
62
|
+
from ._models._cost_report import CostReportOrderData as CostReportOrderData
|
|
63
|
+
from ._models._cost_report import CostReportPosition as CostReportPosition
|
|
64
|
+
from ._models._modifications import (
|
|
65
|
+
CalculatedModificationValues as CalculatedModificationValues,
|
|
66
|
+
)
|
|
67
|
+
from ._models._modifications import (
|
|
68
|
+
CalculatedModificationValuesResponse as CalculatedModificationValuesResponse,
|
|
69
|
+
)
|
|
70
|
+
from ._models._modifications import (
|
|
71
|
+
ModificationCalculationRequest as ModificationCalculationRequest,
|
|
72
|
+
)
|
|
73
|
+
from ._models._modifications import ModificationCapabilities as ModificationCapabilities
|
|
74
|
+
from ._models._modifications import ModificationHistory as ModificationHistory
|
|
75
|
+
from ._models._modifications import ModificationHistoryEntry as ModificationHistoryEntry
|
|
76
|
+
from ._models._modifications import ModificationProperties as ModificationProperties
|
|
77
|
+
from ._models._modifications import ModificationRequest as ModificationRequest
|
|
78
|
+
from ._models._modifications import ModificationValues as ModificationValues
|
|
79
|
+
from ._models._orders import CalculatedOrder as CalculatedOrder
|
|
80
|
+
from ._models._orders import CalculatedOrderRequest as CalculatedOrderRequest
|
|
81
|
+
from ._models._orders import CalculatedRealtimeOrder as CalculatedRealtimeOrder
|
|
82
|
+
from ._models._orders import (
|
|
83
|
+
CalculatedRealtimeOrderRequest as CalculatedRealtimeOrderRequest,
|
|
84
|
+
)
|
|
85
|
+
from ._models._orders import CalculationRealtimeResponse as CalculationRealtimeResponse
|
|
86
|
+
from ._models._orders import CalculationResponse as CalculationResponse
|
|
87
|
+
from ._models._orders import Fee as Fee
|
|
88
|
+
from ._models._orders import Order as Order
|
|
89
|
+
from ._models._orders import OrderRequest as OrderRequest
|
|
90
|
+
from ._models._orders import Orders as Orders
|
|
91
|
+
from ._models._orders import RealtimeOrderRequest as RealtimeOrderRequest
|
|
92
|
+
from ._models._portfolios import CryptoAccount as CryptoAccount
|
|
93
|
+
from ._models._portfolios import Portfolio as Portfolio
|
|
94
|
+
from ._models._portfolios import PortfolioType as PortfolioType
|
|
95
|
+
from ._models._portfolios import RestAccount as RestAccount
|
|
96
|
+
from ._models._portfolios import SecurityAccount as SecurityAccount
|
|
97
|
+
from ._models._profile import (
|
|
98
|
+
CustomerProfileAccountNumbers as CustomerProfileAccountNumbers,
|
|
99
|
+
)
|
|
100
|
+
from ._models._positions import Position as Position
|
|
101
|
+
from ._models._positions import Positions as Positions
|
|
102
|
+
from ._models._tradeability import AssetDetails as AssetDetails
|
|
103
|
+
from ._models._tradeability import Exchange as Exchange
|
|
104
|
+
from ._models._tradeability import RiskHint as RiskHint
|
|
105
|
+
from ._models._tradeability import Tradeability as Tradeability
|
|
106
|
+
from ._models._tradeability import TradeableExchanges as TradeableExchanges
|
|
107
|
+
from ._models._transactions import Transaction as Transaction
|
|
108
|
+
from ._models._transactions import TransactionDetail as TransactionDetail
|
|
109
|
+
from ._models._transactions import TransactionDetailType as TransactionDetailType
|
|
110
|
+
from ._models._transactions import TransactionMode as TransactionMode
|
|
111
|
+
from ._models._transactions import TransactionStatus as TransactionStatus
|
|
112
|
+
from ._models._transactions import TransactionStatusValue as TransactionStatusValue
|
|
113
|
+
from ._resources._account_transactions_requests import (
|
|
114
|
+
AccountTransactionListFilters as AccountTransactionListFilters,
|
|
115
|
+
)
|
|
116
|
+
from ._resources._orders_requests import OrderListFilters as OrderListFilters
|
|
117
|
+
from ._resources._positions_requests import PositionListFilters as PositionListFilters
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
# Clients
|
|
121
|
+
"SmartbrokerClient",
|
|
122
|
+
"AsyncSmartbrokerClient",
|
|
123
|
+
# Auth helpers
|
|
124
|
+
"CachedToken",
|
|
125
|
+
"FileTokenCache",
|
|
126
|
+
"ActiveAccount",
|
|
127
|
+
"ActiveAccountStore",
|
|
128
|
+
"resolve_cache_dir",
|
|
129
|
+
"OAuthClient",
|
|
130
|
+
"AsyncOAuthClient",
|
|
131
|
+
# OAuth PKCE helpers
|
|
132
|
+
"generate_code_verifier",
|
|
133
|
+
"generate_code_challenge",
|
|
134
|
+
# Exceptions
|
|
135
|
+
"SmartbrokerApiError",
|
|
136
|
+
"AuthenticationError",
|
|
137
|
+
"AuthorizationError",
|
|
138
|
+
"NotFoundError",
|
|
139
|
+
"ValidationError",
|
|
140
|
+
"OrderRejectedError",
|
|
141
|
+
"InsufficientBalanceError",
|
|
142
|
+
"BuyingPowerExceededError",
|
|
143
|
+
"InvalidLimitStepError",
|
|
144
|
+
"StopLimitTooLowError",
|
|
145
|
+
"MinimumInvestmentNotReachedError",
|
|
146
|
+
"PennyStockLimitRequiredError",
|
|
147
|
+
"NumbersNotAllowedError",
|
|
148
|
+
"ServerError",
|
|
149
|
+
"TwoFactorError",
|
|
150
|
+
"RefreshTokenExpiredError",
|
|
151
|
+
"TwoFactorTokenExpiredError",
|
|
152
|
+
"RetryExhaustedError",
|
|
153
|
+
# Enums — common
|
|
154
|
+
"OrderDirection",
|
|
155
|
+
"OrderModel",
|
|
156
|
+
"ValidityType",
|
|
157
|
+
"InstrumentType",
|
|
158
|
+
"AssetQuotation",
|
|
159
|
+
"OrderState",
|
|
160
|
+
"TrailingStopType",
|
|
161
|
+
"SortDirection",
|
|
162
|
+
"OrderDirectionFilter",
|
|
163
|
+
"OrderStateScope",
|
|
164
|
+
# Enums — domain
|
|
165
|
+
"PortfolioType",
|
|
166
|
+
"RiskHint",
|
|
167
|
+
"TokenStatus",
|
|
168
|
+
"ModificationRequestStatus",
|
|
169
|
+
"CostReportPositionType",
|
|
170
|
+
"TransactionType",
|
|
171
|
+
# Models — common
|
|
172
|
+
"ProblemDetail",
|
|
173
|
+
"PaginationParams",
|
|
174
|
+
"PaginatedResponse",
|
|
175
|
+
# Models — auth
|
|
176
|
+
"TokenResponse",
|
|
177
|
+
"Token",
|
|
178
|
+
"ApprovalRequiredError",
|
|
179
|
+
# Models — orders
|
|
180
|
+
"OrderRequest",
|
|
181
|
+
"RealtimeOrderRequest",
|
|
182
|
+
"Fee",
|
|
183
|
+
"CalculatedOrder",
|
|
184
|
+
"CalculationResponse",
|
|
185
|
+
"CalculatedRealtimeOrder",
|
|
186
|
+
"CalculationRealtimeResponse",
|
|
187
|
+
"CalculatedOrderRequest",
|
|
188
|
+
"CalculatedRealtimeOrderRequest",
|
|
189
|
+
"Order",
|
|
190
|
+
"Orders",
|
|
191
|
+
# Models — modifications
|
|
192
|
+
"ModificationValues",
|
|
193
|
+
"ModificationCapabilities",
|
|
194
|
+
"ModificationProperties",
|
|
195
|
+
"ModificationCalculationRequest",
|
|
196
|
+
"CalculatedModificationValues",
|
|
197
|
+
"CalculatedModificationValuesResponse",
|
|
198
|
+
"ModificationHistoryEntry",
|
|
199
|
+
"ModificationHistory",
|
|
200
|
+
"ModificationRequest",
|
|
201
|
+
# Models — portfolios
|
|
202
|
+
"RestAccount",
|
|
203
|
+
"SecurityAccount",
|
|
204
|
+
"CryptoAccount",
|
|
205
|
+
"Portfolio",
|
|
206
|
+
# Models — profile
|
|
207
|
+
"CustomerProfileAccountNumbers",
|
|
208
|
+
# Models — positions
|
|
209
|
+
"Position",
|
|
210
|
+
"Positions",
|
|
211
|
+
# Models — tradeability
|
|
212
|
+
"AssetDetails",
|
|
213
|
+
"Exchange",
|
|
214
|
+
"TradeableExchanges",
|
|
215
|
+
"Tradeability",
|
|
216
|
+
# Filters
|
|
217
|
+
"OrderListFilters",
|
|
218
|
+
"PositionListFilters",
|
|
219
|
+
"AccountTransactionListFilters",
|
|
220
|
+
# Models — cost report
|
|
221
|
+
"CostReport",
|
|
222
|
+
"CostReportOrderData",
|
|
223
|
+
"CostReportPosition",
|
|
224
|
+
# Models — account transactions
|
|
225
|
+
"AccountTransactions",
|
|
226
|
+
"RestAccountTransaction",
|
|
227
|
+
# Models — marketstate
|
|
228
|
+
"Marketstate",
|
|
229
|
+
"QuoteData",
|
|
230
|
+
"CloseData",
|
|
231
|
+
# Models — transactions
|
|
232
|
+
"Transaction",
|
|
233
|
+
"TransactionDetail",
|
|
234
|
+
"TransactionDetailType",
|
|
235
|
+
"TransactionMode",
|
|
236
|
+
"TransactionStatus",
|
|
237
|
+
"TransactionStatusValue",
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
__import__("logging").getLogger(__name__).addHandler(
|
|
241
|
+
__import__("logging").NullHandler(),
|
|
242
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Account preferences package — preference model and disk store."""
|
|
2
|
+
|
|
3
|
+
from smartbroker_plus_sdk._auth._account_prefs._account_preferences import (
|
|
4
|
+
AccountPreferences as AccountPreferences,
|
|
5
|
+
)
|
|
6
|
+
from smartbroker_plus_sdk._auth._account_prefs._account_prefs_store import (
|
|
7
|
+
AccountPreferencesStore as AccountPreferencesStore,
|
|
8
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""AccountPreferences — persisted environment preference for a login account."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class AccountPreferences:
|
|
9
|
+
"""Persisted environment preference for a single cached account.
|
|
10
|
+
|
|
11
|
+
Exactly one of ``environment`` or ``auth_base_override`` must be non-None.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
schema_version: Schema format version; must be 1.
|
|
15
|
+
environment: Named environment (``"sand"`` or ``"prod"``), or None.
|
|
16
|
+
auth_base_override: Custom auth base URL, or None.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
schema_version: int
|
|
20
|
+
environment: Optional[str]
|
|
21
|
+
auth_base_override: Optional[str]
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, object]:
|
|
24
|
+
"""Serialize to a plain dict for JSON storage.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dict including ``schema_version: 1``.
|
|
28
|
+
"""
|
|
29
|
+
return {
|
|
30
|
+
"schema_version": self.schema_version,
|
|
31
|
+
"environment": self.environment,
|
|
32
|
+
"auth_base_override": self.auth_base_override,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, source: dict[str, object]) -> "AccountPreferences":
|
|
37
|
+
"""Deserialize from a dict.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
source: Raw dict, typically from JSON.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A validated AccountPreferences instance.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
TypeError: If ``schema_version`` is missing or not equal to 1.
|
|
47
|
+
ValueError: If invariant is violated (both fields None or both set).
|
|
48
|
+
"""
|
|
49
|
+
version = source.get("schema_version")
|
|
50
|
+
if version != 1:
|
|
51
|
+
raise TypeError(
|
|
52
|
+
f"Unsupported or missing schema_version: {version!r}. Expected 1."
|
|
53
|
+
)
|
|
54
|
+
environment = source.get("environment")
|
|
55
|
+
auth_base_override = source.get("auth_base_override")
|
|
56
|
+
# Normalise absent keys to None
|
|
57
|
+
env_val = environment if isinstance(environment, str) else None
|
|
58
|
+
auth_val = auth_base_override if isinstance(auth_base_override, str) else None
|
|
59
|
+
if (env_val is None) == (auth_val is None):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"Exactly one of environment or auth_base_override must be set; "
|
|
62
|
+
f"got environment={env_val!r}, auth_base_override={auth_val!r}"
|
|
63
|
+
)
|
|
64
|
+
return cls(
|
|
65
|
+
schema_version=1,
|
|
66
|
+
environment=env_val,
|
|
67
|
+
auth_base_override=auth_val,
|
|
68
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""AccountPreferencesStore — disk-backed store for per-account preferences."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from smartbroker_plus_sdk._auth._account_prefs._account_preferences import (
|
|
12
|
+
AccountPreferences,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_FILENAME = "account_prefs.json"
|
|
18
|
+
_DIR_MODE = 0o700
|
|
19
|
+
_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR # 0o600
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AccountPreferencesStore:
|
|
23
|
+
"""Persists per-account environment preferences as JSON on disk.
|
|
24
|
+
|
|
25
|
+
The store uses a single flat JSON file keyed by ``cache_key_for(account)``
|
|
26
|
+
(``base_url|client_id``).
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
cache_dir: Directory that will contain ``account_prefs.json``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, cache_dir: Path) -> None:
|
|
33
|
+
self._cache_dir = cache_dir
|
|
34
|
+
self._path = cache_dir / _FILENAME
|
|
35
|
+
|
|
36
|
+
def load(self, key: str) -> Optional[AccountPreferences]:
|
|
37
|
+
"""Load preferences for the given account key.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
key: Cache key (``base_url|client_id``).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The stored preferences, or None if the file is missing, corrupt,
|
|
44
|
+
or the entry is malformed. Never raises.
|
|
45
|
+
"""
|
|
46
|
+
store = self._read_store()
|
|
47
|
+
entry = store.get(key)
|
|
48
|
+
if entry is None:
|
|
49
|
+
return None
|
|
50
|
+
if not isinstance(entry, dict):
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
return AccountPreferences.from_dict(entry)
|
|
54
|
+
except (TypeError, ValueError, KeyError):
|
|
55
|
+
logger.warning("Malformed account_prefs entry for key %r", key)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def save(self, key: str, prefs: AccountPreferences) -> None:
|
|
59
|
+
"""Persist preferences for the given account key.
|
|
60
|
+
|
|
61
|
+
Writes atomically via a temp file. Directory is created with mode
|
|
62
|
+
``0700``; file is written with mode ``0600``.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
key: Cache key (``base_url|client_id``).
|
|
66
|
+
prefs: Preferences to store.
|
|
67
|
+
"""
|
|
68
|
+
store = self._read_store()
|
|
69
|
+
store[key] = prefs.to_dict()
|
|
70
|
+
self._write_store(store)
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Internal helpers
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _read_store(self) -> dict[str, object]:
|
|
77
|
+
if not self._path.exists():
|
|
78
|
+
return {}
|
|
79
|
+
try:
|
|
80
|
+
raw = self._path.read_text(encoding="utf-8")
|
|
81
|
+
except OSError:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Unreadable account_prefs at %s; treating as empty", self._path
|
|
84
|
+
)
|
|
85
|
+
return {}
|
|
86
|
+
try:
|
|
87
|
+
loaded = json.loads(raw)
|
|
88
|
+
except json.JSONDecodeError:
|
|
89
|
+
logger.warning("Corrupt account_prefs at %s; treating as empty", self._path)
|
|
90
|
+
return {}
|
|
91
|
+
if not isinstance(loaded, dict):
|
|
92
|
+
return {}
|
|
93
|
+
return dict(loaded)
|
|
94
|
+
|
|
95
|
+
def _write_store(self, store: dict[str, object]) -> None:
|
|
96
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True, mode=_DIR_MODE)
|
|
97
|
+
os.chmod(self._cache_dir, _DIR_MODE)
|
|
98
|
+
|
|
99
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
100
|
+
dir=str(self._cache_dir),
|
|
101
|
+
suffix=".tmp",
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
self._flush_and_replace(fd, tmp_path, store)
|
|
105
|
+
except BaseException:
|
|
106
|
+
_cleanup_temp(fd, tmp_path)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
def _flush_and_replace(
|
|
110
|
+
self,
|
|
111
|
+
fd: int,
|
|
112
|
+
tmp_path: str,
|
|
113
|
+
store: dict[str, object],
|
|
114
|
+
) -> None:
|
|
115
|
+
encoded = json.dumps(store, indent=2).encode("utf-8")
|
|
116
|
+
os.write(fd, encoded)
|
|
117
|
+
os.close(fd)
|
|
118
|
+
os.chmod(tmp_path, _FILE_MODE)
|
|
119
|
+
os.replace(tmp_path, str(self._path))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cleanup_temp(fd: int, tmp_path: str) -> None:
|
|
123
|
+
_try_close_fd(fd)
|
|
124
|
+
if os.path.exists(tmp_path):
|
|
125
|
+
os.unlink(tmp_path)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _try_close_fd(fd: int) -> None:
|
|
129
|
+
try:
|
|
130
|
+
os.fstat(fd)
|
|
131
|
+
except OSError:
|
|
132
|
+
return
|
|
133
|
+
os.close(fd)
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class ActiveAccount:
|
|
6
|
+
"""Identifies a specific Smartbroker+ account environment."""
|
|
7
|
+
|
|
8
|
+
base_url: str
|
|
9
|
+
client_id: str
|
|
10
|
+
|
|
11
|
+
def to_dict(self) -> dict[str, str]:
|
|
12
|
+
"""Serialize to a plain dict for JSON storage."""
|
|
13
|
+
return {"base_url": self.base_url, "client_id": self.client_id}
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dict(cls, source: dict[str, object]) -> "ActiveAccount":
|
|
17
|
+
"""Deserialize from a dict, raising ValueError on invalid data."""
|
|
18
|
+
base_url = source.get("base_url")
|
|
19
|
+
client_id = source.get("client_id")
|
|
20
|
+
if not isinstance(base_url, str) or not isinstance(client_id, str):
|
|
21
|
+
raise ValueError("Invalid active account data")
|
|
22
|
+
return cls(base_url=base_url, client_id=client_id)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from smartbroker_plus_sdk._auth._active_account._active_account import ActiveAccount
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_FILENAME = "active_account.json"
|
|
13
|
+
_DIR_MODE = 0o700
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ActiveAccountStore:
|
|
17
|
+
"""Persists the currently active account pointer to disk."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, cache_dir: Path) -> None:
|
|
20
|
+
self._path = cache_dir / _FILENAME
|
|
21
|
+
|
|
22
|
+
def load(self) -> Optional[ActiveAccount]:
|
|
23
|
+
"""Load the active account, or None if missing/corrupt."""
|
|
24
|
+
if not self._path.exists():
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
raw = json.loads(self._path.read_text(encoding="utf-8"))
|
|
28
|
+
except (json.JSONDecodeError, OSError):
|
|
29
|
+
logger.warning("Corrupt active account file at %s", self._path)
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return ActiveAccount.from_dict(raw)
|
|
33
|
+
except (ValueError, KeyError):
|
|
34
|
+
logger.warning("Corrupt active account file at %s", self._path)
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def save(self, account: ActiveAccount) -> None:
|
|
38
|
+
"""Persist the active account pointer."""
|
|
39
|
+
self._path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_MODE)
|
|
40
|
+
fd, staging_path = tempfile.mkstemp(
|
|
41
|
+
dir=str(self._path.parent),
|
|
42
|
+
suffix=".tmp",
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
self._write_and_move(fd, staging_path, account)
|
|
46
|
+
except BaseException:
|
|
47
|
+
if os.path.exists(staging_path):
|
|
48
|
+
os.unlink(staging_path)
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
def clear(self) -> None:
|
|
52
|
+
"""Remove the active account pointer. Idempotent."""
|
|
53
|
+
self._path.unlink(missing_ok=True)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def path(self) -> Path:
|
|
57
|
+
"""The path to the active account file."""
|
|
58
|
+
return self._path
|
|
59
|
+
|
|
60
|
+
def _write_and_move(
|
|
61
|
+
self,
|
|
62
|
+
fd: int,
|
|
63
|
+
staging_path: str,
|
|
64
|
+
account: ActiveAccount,
|
|
65
|
+
) -> None:
|
|
66
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fobj:
|
|
67
|
+
json.dump(account.to_dict(), fobj)
|
|
68
|
+
os.replace(staging_path, str(self._path))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""OAuth environment resolver — Environment enum, AuthEndpoints, resolve().
|
|
2
|
+
|
|
3
|
+
This module is shared across the CLI and sandbox test harness; it does not
|
|
4
|
+
belong inside ``_cli/``. CLI code imports from here via the thin re-export
|
|
5
|
+
in ``_cli/_environments.py``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
_REALM = "smartbroker"
|
|
13
|
+
_API_URL = "https://api.{env}.paas.winning.wocio.de"
|
|
14
|
+
_AUTH_BASE_URL = "https://auth.{env}.paas.winning.wocio.de/realms/{realm}"
|
|
15
|
+
_TOKEN_SUFFIX = "/protocol/openid-connect/token"
|
|
16
|
+
_LOGOUT_SUFFIX = "/protocol/openid-connect/logout"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Environment(str, Enum):
|
|
20
|
+
"""Supported deployment environments."""
|
|
21
|
+
|
|
22
|
+
SAND = "sand"
|
|
23
|
+
PROD = "prod"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class AuthEndpoints:
|
|
28
|
+
"""Resolved OAuth endpoint URLs for a given environment."""
|
|
29
|
+
|
|
30
|
+
api_base_url: str
|
|
31
|
+
auth_url: str
|
|
32
|
+
token_url: str
|
|
33
|
+
logout_url: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_custom(auth_base: str) -> AuthEndpoints:
|
|
37
|
+
"""Resolve OAuth endpoint URLs for a fully custom auth base URL.
|
|
38
|
+
|
|
39
|
+
Used when neither the sand nor prod environment is appropriate and the
|
|
40
|
+
caller supplies an explicit auth server base (e.g. a self-hosted Keycloak
|
|
41
|
+
realm URL). The API base URL is set to ``auth_base`` because no
|
|
42
|
+
environment-derived API URL is available in this mode.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
auth_base: The auth server base URL ending in the realm path segment
|
|
46
|
+
(e.g. ``https://sso.example.com/realms/myapp``).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A fully populated AuthEndpoints value object.
|
|
50
|
+
"""
|
|
51
|
+
return AuthEndpoints(
|
|
52
|
+
api_base_url=auth_base,
|
|
53
|
+
auth_url=auth_base,
|
|
54
|
+
token_url=f"{auth_base}{_TOKEN_SUFFIX}",
|
|
55
|
+
logout_url=f"{auth_base}{_LOGOUT_SUFFIX}",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve(
|
|
60
|
+
env: Environment,
|
|
61
|
+
override_auth_base: Optional[str] = None,
|
|
62
|
+
) -> AuthEndpoints:
|
|
63
|
+
"""Resolve OAuth endpoint URLs for the given environment.
|
|
64
|
+
|
|
65
|
+
When ``override_auth_base`` is supplied it must end in
|
|
66
|
+
``/realms/<realm-name>`` (e.g. ``https://sso.example.com/realms/myapp``)
|
|
67
|
+
because the token and logout URL suffixes are appended to it verbatim.
|
|
68
|
+
Supplying a bare base URL (without the realm path segment) will produce
|
|
69
|
+
malformed endpoint URLs.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
env: The target deployment environment.
|
|
73
|
+
override_auth_base: When provided, replaces the derived auth base URL
|
|
74
|
+
for auth_url, token_url, and logout_url. The api_base_url is
|
|
75
|
+
always derived from env.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A fully populated AuthEndpoints value object.
|
|
79
|
+
"""
|
|
80
|
+
api_base_url = _API_URL.format(env=env.value)
|
|
81
|
+
auth_base = (
|
|
82
|
+
_AUTH_BASE_URL.format(env=env.value, realm=_REALM)
|
|
83
|
+
if override_auth_base is None
|
|
84
|
+
else override_auth_base
|
|
85
|
+
)
|
|
86
|
+
return AuthEndpoints(
|
|
87
|
+
api_base_url=api_base_url,
|
|
88
|
+
auth_url=auth_base,
|
|
89
|
+
token_url=f"{auth_base}{_TOKEN_SUFFIX}",
|
|
90
|
+
logout_url=f"{auth_base}{_LOGOUT_SUFFIX}",
|
|
91
|
+
)
|