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.
Files changed (110) hide show
  1. smartbroker_plus_sdk/__init__.py +242 -0
  2. smartbroker_plus_sdk/__main__.py +9 -0
  3. smartbroker_plus_sdk/_auth/__init__.py +0 -0
  4. smartbroker_plus_sdk/_auth/_account_prefs/__init__.py +8 -0
  5. smartbroker_plus_sdk/_auth/_account_prefs/_account_preferences.py +68 -0
  6. smartbroker_plus_sdk/_auth/_account_prefs/_account_prefs_store.py +133 -0
  7. smartbroker_plus_sdk/_auth/_active_account/__init__.py +0 -0
  8. smartbroker_plus_sdk/_auth/_active_account/_active_account.py +22 -0
  9. smartbroker_plus_sdk/_auth/_active_account/_active_account_store.py +68 -0
  10. smartbroker_plus_sdk/_auth/_environments.py +91 -0
  11. smartbroker_plus_sdk/_auth/_oauth.py +505 -0
  12. smartbroker_plus_sdk/_auth/_token_cache/__init__.py +0 -0
  13. smartbroker_plus_sdk/_auth/_token_cache/_cached_token.py +103 -0
  14. smartbroker_plus_sdk/_auth/_token_cache/_file_token_cache.py +155 -0
  15. smartbroker_plus_sdk/_auth/_token_cache/_null_token_cache.py +49 -0
  16. smartbroker_plus_sdk/_auth/_token_cache/_path_resolver.py +32 -0
  17. smartbroker_plus_sdk/_auth/_token_cache/_token_cache.py +48 -0
  18. smartbroker_plus_sdk/_auth/_two_factor.py +494 -0
  19. smartbroker_plus_sdk/_auth/_two_factor_poll.py +99 -0
  20. smartbroker_plus_sdk/_auth/_two_factor_provider.py +52 -0
  21. smartbroker_plus_sdk/_auth/_two_factor_requests.py +119 -0
  22. smartbroker_plus_sdk/_cli/__init__.py +0 -0
  23. smartbroker_plus_sdk/_cli/_account_resolver.py +65 -0
  24. smartbroker_plus_sdk/_cli/_account_transactions_cmd.py +53 -0
  25. smartbroker_plus_sdk/_cli/_app.py +153 -0
  26. smartbroker_plus_sdk/_cli/_auth_cmd.py +325 -0
  27. smartbroker_plus_sdk/_cli/_client_factory.py +181 -0
  28. smartbroker_plus_sdk/_cli/_context.py +23 -0
  29. smartbroker_plus_sdk/_cli/_cost_report_cmd.py +89 -0
  30. smartbroker_plus_sdk/_cli/_entrypoint.py +42 -0
  31. smartbroker_plus_sdk/_cli/_environments.py +10 -0
  32. smartbroker_plus_sdk/_cli/_error_handler.py +184 -0
  33. smartbroker_plus_sdk/_cli/_health_cmd.py +20 -0
  34. smartbroker_plus_sdk/_cli/_logging_setup.py +61 -0
  35. smartbroker_plus_sdk/_cli/_marketstate_cmd.py +44 -0
  36. smartbroker_plus_sdk/_cli/_modifications_cmd.py +194 -0
  37. smartbroker_plus_sdk/_cli/_oauth_browser.py +96 -0
  38. smartbroker_plus_sdk/_cli/_orders_cmd.py +167 -0
  39. smartbroker_plus_sdk/_cli/_output.py +166 -0
  40. smartbroker_plus_sdk/_cli/_output_format.py +12 -0
  41. smartbroker_plus_sdk/_cli/_payload_loader.py +59 -0
  42. smartbroker_plus_sdk/_cli/_portfolios_cmd.py +77 -0
  43. smartbroker_plus_sdk/_cli/_positions_cmd.py +127 -0
  44. smartbroker_plus_sdk/_cli/_profile_cmd.py +40 -0
  45. smartbroker_plus_sdk/_cli/_prompts.py +32 -0
  46. smartbroker_plus_sdk/_cli/_tradeability_cmd.py +71 -0
  47. smartbroker_plus_sdk/_cli/_transactions_cmd.py +77 -0
  48. smartbroker_plus_sdk/_client.py +505 -0
  49. smartbroker_plus_sdk/_error_mapping.py +112 -0
  50. smartbroker_plus_sdk/_exceptions.py +124 -0
  51. smartbroker_plus_sdk/_models/__init__.py +0 -0
  52. smartbroker_plus_sdk/_models/_account_transactions.py +42 -0
  53. smartbroker_plus_sdk/_models/_auth.py +65 -0
  54. smartbroker_plus_sdk/_models/_common.py +366 -0
  55. smartbroker_plus_sdk/_models/_cost_report.py +49 -0
  56. smartbroker_plus_sdk/_models/_marketstate.py +46 -0
  57. smartbroker_plus_sdk/_models/_modifications.py +162 -0
  58. smartbroker_plus_sdk/_models/_orders.py +318 -0
  59. smartbroker_plus_sdk/_models/_portfolios.py +87 -0
  60. smartbroker_plus_sdk/_models/_positions.py +50 -0
  61. smartbroker_plus_sdk/_models/_profile.py +13 -0
  62. smartbroker_plus_sdk/_models/_tradeability.py +79 -0
  63. smartbroker_plus_sdk/_models/_transactions.py +63 -0
  64. smartbroker_plus_sdk/_resources/__init__.py +0 -0
  65. smartbroker_plus_sdk/_resources/_account_transactions.py +96 -0
  66. smartbroker_plus_sdk/_resources/_account_transactions_requests.py +71 -0
  67. smartbroker_plus_sdk/_resources/_b2b_base.py +2 -0
  68. smartbroker_plus_sdk/_resources/_cost_report.py +86 -0
  69. smartbroker_plus_sdk/_resources/_cost_report_requests.py +31 -0
  70. smartbroker_plus_sdk/_resources/_health.py +47 -0
  71. smartbroker_plus_sdk/_resources/_health_requests.py +15 -0
  72. smartbroker_plus_sdk/_resources/_marketstate.py +70 -0
  73. smartbroker_plus_sdk/_resources/_marketstate_requests.py +33 -0
  74. smartbroker_plus_sdk/_resources/_order_modifications.py +260 -0
  75. smartbroker_plus_sdk/_resources/_order_modifications_requests.py +128 -0
  76. smartbroker_plus_sdk/_resources/_orders.py +545 -0
  77. smartbroker_plus_sdk/_resources/_orders_requests.py +272 -0
  78. smartbroker_plus_sdk/_resources/_portfolios.py +50 -0
  79. smartbroker_plus_sdk/_resources/_portfolios_requests.py +27 -0
  80. smartbroker_plus_sdk/_resources/_positions.py +289 -0
  81. smartbroker_plus_sdk/_resources/_positions_requests.py +89 -0
  82. smartbroker_plus_sdk/_resources/_profile.py +56 -0
  83. smartbroker_plus_sdk/_resources/_profile_requests.py +17 -0
  84. smartbroker_plus_sdk/_resources/_request.py +35 -0
  85. smartbroker_plus_sdk/_resources/_tradeability.py +122 -0
  86. smartbroker_plus_sdk/_resources/_tradeability_requests.py +91 -0
  87. smartbroker_plus_sdk/_resources/_transactions.py +95 -0
  88. smartbroker_plus_sdk/_resources/_transactions_requests.py +34 -0
  89. smartbroker_plus_sdk/_resources/_validation.py +57 -0
  90. smartbroker_plus_sdk/_transport/__init__.py +0 -0
  91. smartbroker_plus_sdk/_transport/_auth_policy.py +182 -0
  92. smartbroker_plus_sdk/_transport/_http_transport.py +530 -0
  93. smartbroker_plus_sdk/_transport/_log_formatting.py +345 -0
  94. smartbroker_plus_sdk/_transport/_logging_transport.py +83 -0
  95. smartbroker_plus_sdk/_transport/_logging_transport_async.py +83 -0
  96. smartbroker_plus_sdk/_transport/_response.py +25 -0
  97. smartbroker_plus_sdk/_transport/_retry.py +18 -0
  98. smartbroker_plus_sdk/_transport/_two_factor_guard.py +204 -0
  99. smartbroker_plus_sdk/_transport/_urls.py +22 -0
  100. smartbroker_plus_sdk/account/__init__.py +46 -0
  101. smartbroker_plus_sdk/exceptions.py +43 -0
  102. smartbroker_plus_sdk/market/__init__.py +29 -0
  103. smartbroker_plus_sdk/py.typed +0 -0
  104. smartbroker_plus_sdk/trading/__init__.py +87 -0
  105. smartbroker_plus_sdk-0.3.0.dist-info/METADATA +250 -0
  106. smartbroker_plus_sdk-0.3.0.dist-info/RECORD +110 -0
  107. smartbroker_plus_sdk-0.3.0.dist-info/WHEEL +5 -0
  108. smartbroker_plus_sdk-0.3.0.dist-info/entry_points.txt +2 -0
  109. smartbroker_plus_sdk-0.3.0.dist-info/licenses/LICENSE +21 -0
  110. 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
+ )
@@ -0,0 +1,9 @@
1
+ """Entry point for ``python -m smartbroker_plus_sdk``."""
2
+
3
+
4
+ def main() -> None:
5
+ print("hello from smartbroker_plus_sdk") # noqa: WPS421
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
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
+ )