publicdotcom-cli 1.0.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 (146) hide show
  1. publicdotcom_cli/__init__.py +3 -0
  2. publicdotcom_cli/__main__.py +5 -0
  3. publicdotcom_cli/_generated/__init__.py +8 -0
  4. publicdotcom_cli/_generated/api/__init__.py +1 -0
  5. publicdotcom_cli/_generated/api/account_details/__init__.py +1 -0
  6. publicdotcom_cli/_generated/api/account_details/get_account_portfolio_v2.py +182 -0
  7. publicdotcom_cli/_generated/api/account_details/get_history.py +250 -0
  8. publicdotcom_cli/_generated/api/authorization/__init__.py +1 -0
  9. publicdotcom_cli/_generated/api/authorization/create_personal_access_token.py +230 -0
  10. publicdotcom_cli/_generated/api/instrument_details/__init__.py +1 -0
  11. publicdotcom_cli/_generated/api/instrument_details/get_all_instruments.py +303 -0
  12. publicdotcom_cli/_generated/api/instrument_details/get_instrument.py +170 -0
  13. publicdotcom_cli/_generated/api/list_accounts/__init__.py +1 -0
  14. publicdotcom_cli/_generated/api/list_accounts/get_accounts.py +172 -0
  15. publicdotcom_cli/_generated/api/market_data/__init__.py +1 -0
  16. publicdotcom_cli/_generated/api/market_data/get_option_chain.py +200 -0
  17. publicdotcom_cli/_generated/api/market_data/get_option_expirations.py +210 -0
  18. publicdotcom_cli/_generated/api/market_data/get_quotes.py +200 -0
  19. publicdotcom_cli/_generated/api/option_details/__init__.py +1 -0
  20. publicdotcom_cli/_generated/api/option_details/get_option_greeks.py +194 -0
  21. publicdotcom_cli/_generated/api/order_placement/__init__.py +1 -0
  22. publicdotcom_cli/_generated/api/order_placement/cancel_order.py +123 -0
  23. publicdotcom_cli/_generated/api/order_placement/get_order.py +206 -0
  24. publicdotcom_cli/_generated/api/order_placement/place_multileg_order.py +214 -0
  25. publicdotcom_cli/_generated/api/order_placement/place_order.py +222 -0
  26. publicdotcom_cli/_generated/api/order_placement/preflight_multi_leg.py +276 -0
  27. publicdotcom_cli/_generated/api/order_placement/preflight_single_leg.py +220 -0
  28. publicdotcom_cli/_generated/api/order_placement/replace_order.py +222 -0
  29. publicdotcom_cli/_generated/client.py +272 -0
  30. publicdotcom_cli/_generated/errors.py +16 -0
  31. publicdotcom_cli/_generated/models/__init__.py +417 -0
  32. publicdotcom_cli/_generated/models/com_hellopublic_holdingsystem_core_types_option_price_increment.py +71 -0
  33. publicdotcom_cli/_generated/models/com_hellopublic_userapiauthservice_api_personal_create_access_token_request.py +84 -0
  34. publicdotcom_cli/_generated/models/com_hellopublic_userapiauthservice_api_personal_create_access_token_response.py +61 -0
  35. publicdotcom_cli/_generated/models/com_hellopublic_userapiauthservice_domain_error_error_body.py +70 -0
  36. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_account_account_settings.py +161 -0
  37. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_account_account_settings_account_type.py +14 -0
  38. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_account_account_settings_brokerage_account_type.py +9 -0
  39. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_account_account_settings_options_level.py +12 -0
  40. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_account_account_settings_response.py +85 -0
  41. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_account_account_settings_trade_permissions.py +11 -0
  42. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_response_page.py +195 -0
  43. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_transaction.py +263 -0
  44. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_transaction_direction.py +9 -0
  45. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_transaction_security_type.py +13 -0
  46. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_transaction_side.py +9 -0
  47. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_transaction_sub_type.py +19 -0
  48. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_history_gateway_history_transaction_type.py +10 -0
  49. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_option_chain_request.py +85 -0
  50. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_option_chain_response.py +115 -0
  51. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_option_expirations_request.py +75 -0
  52. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_option_expirations_response.py +81 -0
  53. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_quote.py +272 -0
  54. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_quote_outcome.py +9 -0
  55. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_quote_request.py +84 -0
  56. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_gateway_quote_response.py +87 -0
  57. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_one_day_change.py +74 -0
  58. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_marketdata_quote_option_details.py +135 -0
  59. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_options_greek_response.py +113 -0
  60. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_options_greeks_response.py +86 -0
  61. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_options_option_greeks_type_0.py +117 -0
  62. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_cancel_replace_order_request.py +139 -0
  63. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_cancel_replace_order_request_order_type.py +11 -0
  64. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_dto.py +265 -0
  65. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_dto_fractional_trading.py +10 -0
  66. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_dto_option_spread_trading.py +10 -0
  67. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_dto_option_trading.py +10 -0
  68. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_dto_shorting_availability.py +10 -0
  69. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_dto_trading.py +10 -0
  70. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_instrument_response.py +87 -0
  71. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_multileg_order_request.py +141 -0
  72. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_multileg_order_request_type.py +11 -0
  73. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_order_request.py +231 -0
  74. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_order_request_equity_market_session.py +9 -0
  75. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_order_request_open_close_indicator.py +9 -0
  76. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_order_request_order_side.py +9 -0
  77. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_order_request_order_type.py +11 -0
  78. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_api_order_result.py +69 -0
  79. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_leg_instrument.py +73 -0
  80. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_leg_instrument_type.py +9 -0
  81. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order.py +334 -0
  82. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_instrument.py +73 -0
  83. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_instrument_type.py +15 -0
  84. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_leg.py +125 -0
  85. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_leg_open_close_indicator.py +9 -0
  86. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_leg_side.py +9 -0
  87. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_open_close_indicator.py +9 -0
  88. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_side.py +9 -0
  89. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_status.py +17 -0
  90. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_order_type.py +11 -0
  91. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_short_selling.py +127 -0
  92. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_short_selling_availability.py +10 -0
  93. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_gateway_short_selling_uptick_rule.py +9 -0
  94. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_instrumentdetails_api_instrument_details.py +67 -0
  95. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_instrumentdetails_api_instrument_details_bond.py +74 -0
  96. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_instrumentdetails_api_instrument_details_crypto.py +92 -0
  97. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_order_expiration.py +87 -0
  98. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_order_order_expiration_time_in_force.py +9 -0
  99. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gain_type_0.py +88 -0
  100. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_buying_power.py +77 -0
  101. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_cost_basis_type_0.py +167 -0
  102. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_account_v2.py +233 -0
  103. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_account_v2_account_type.py +14 -0
  104. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_equity_v2.py +100 -0
  105. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_equity_v2_type.py +14 -0
  106. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_instrument.py +93 -0
  107. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_instrument_type.py +14 -0
  108. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_portfolio_position.py +352 -0
  109. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_strategy.py +276 -0
  110. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_gateway_strategy_leg.py +78 -0
  111. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_portfolio_price_type_0.py +79 -0
  112. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_margin_impact.py +70 -0
  113. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_margin_requirement.py +70 -0
  114. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_option_details.py +91 -0
  115. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_option_details_type.py +9 -0
  116. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_option_rebate.py +79 -0
  117. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_price_increment.py +80 -0
  118. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_gateway_regulatory_fees.py +118 -0
  119. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_leg_response.py +167 -0
  120. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_leg_response_open_close_indicator.py +9 -0
  121. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_leg_response_side.py +9 -0
  122. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_multi_leg_request.py +148 -0
  123. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_multi_leg_request_order_type.py +11 -0
  124. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_multi_leg_response.py +313 -0
  125. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_single_leg_request.py +225 -0
  126. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_single_leg_request_equity_market_session.py +11 -0
  127. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_single_leg_request_open_close_indicator.py +11 -0
  128. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_single_leg_request_order_side.py +9 -0
  129. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_single_leg_request_order_type.py +11 -0
  130. publicdotcom_cli/_generated/models/com_hellopublic_userapigateway_api_rest_preflight_preflight_single_leg_response.py +410 -0
  131. publicdotcom_cli/_generated/models/get_all_instruments_fractional_trading_filter_item.py +10 -0
  132. publicdotcom_cli/_generated/models/get_all_instruments_option_spread_trading_filter_item.py +10 -0
  133. publicdotcom_cli/_generated/models/get_all_instruments_option_trading_filter_item.py +10 -0
  134. publicdotcom_cli/_generated/models/get_all_instruments_trading_filter_item.py +10 -0
  135. publicdotcom_cli/_generated/models/get_all_instruments_type_filter_item.py +15 -0
  136. publicdotcom_cli/_generated/models/get_instrument_type.py +15 -0
  137. publicdotcom_cli/_generated/types.py +54 -0
  138. publicdotcom_cli/cli.py +761 -0
  139. publicdotcom_cli/client.py +83 -0
  140. publicdotcom_cli/config.py +222 -0
  141. publicdotcom_cli/output.py +77 -0
  142. publicdotcom_cli/payloads.py +36 -0
  143. publicdotcom_cli-1.0.0.dist-info/METADATA +256 -0
  144. publicdotcom_cli-1.0.0.dist-info/RECORD +146 -0
  145. publicdotcom_cli-1.0.0.dist-info/WHEEL +4 -0
  146. publicdotcom_cli-1.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,761 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from getpass import getpass
5
+ from pathlib import Path
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+
10
+ from publicdotcom_cli import __version__
11
+ from publicdotcom_cli.client import ApiClient, ApiError, MissingTokenError
12
+ from publicdotcom_cli.config import (
13
+ ACCOUNT_ID_ENV_VAR,
14
+ AUTO_REFRESH_ENV_VAR,
15
+ BASE_URL_ENV_VAR,
16
+ DEFAULT_BASE_URL,
17
+ SECRET_ENV_VAR,
18
+ TOKEN_ENV_VAR,
19
+ clear_default_account_id,
20
+ clear_personal_secret,
21
+ clear_token,
22
+ get_default_account_id,
23
+ get_personal_secret,
24
+ get_token,
25
+ mask_token,
26
+ set_default_account_id,
27
+ set_personal_secret,
28
+ set_token,
29
+ token_expires_soon,
30
+ )
31
+ from publicdotcom_cli.output import (
32
+ console,
33
+ exit_with_error,
34
+ print_accounts,
35
+ print_error,
36
+ print_json,
37
+ print_quotes,
38
+ )
39
+ from publicdotcom_cli.payloads import ensure_order_id, instrument, instruments, load_json_file
40
+
41
+ app = typer.Typer(help="CLI for the Public API.")
42
+ auth_app = typer.Typer(help="Authentication commands.")
43
+ accounts_app = typer.Typer(help="Account commands.")
44
+ portfolio_app = typer.Typer(help="Portfolio commands.")
45
+ history_app = typer.Typer(help="Account history commands.")
46
+ instruments_app = typer.Typer(help="Instrument lookup commands.")
47
+ market_app = typer.Typer(help="Market data commands.")
48
+ options_app = typer.Typer(help="Option details commands.")
49
+ order_app = typer.Typer(help="Order and preflight commands.")
50
+
51
+ app.add_typer(auth_app, name="auth")
52
+ app.add_typer(accounts_app, name="accounts")
53
+ app.add_typer(portfolio_app, name="portfolio")
54
+ app.add_typer(history_app, name="history")
55
+ app.add_typer(instruments_app, name="instruments")
56
+ app.add_typer(market_app, name="market")
57
+ app.add_typer(options_app, name="options")
58
+ app.add_typer(order_app, name="order")
59
+
60
+
61
+ @dataclass
62
+ class RuntimeConfig:
63
+ base_url: str
64
+ token: str | None
65
+ personal_secret: str | None
66
+ default_account_id: str | None
67
+ timeout: float
68
+ json_output: bool
69
+ auto_refresh: bool
70
+ refresh_validity_minutes: int
71
+ refresh_skew_seconds: int
72
+
73
+
74
+ def _runtime(ctx: typer.Context) -> RuntimeConfig:
75
+ runtime = ctx.find_root().obj
76
+ if not isinstance(runtime, RuntimeConfig):
77
+ raise RuntimeError("CLI runtime was not initialized")
78
+ return runtime
79
+
80
+
81
+ def _api(ctx: typer.Context) -> ApiClient:
82
+ runtime = _runtime(ctx)
83
+ return ApiClient(base_url=runtime.base_url, token=runtime.token, timeout=runtime.timeout)
84
+
85
+
86
+ def _resolve_account_id(ctx: typer.Context, account_id: str | None) -> str:
87
+ resolved = account_id or _runtime(ctx).default_account_id
88
+ if not resolved:
89
+ exit_with_error(
90
+ "No account ID provided. Pass --account-id, set PUBLIC_ACCOUNT_ID, "
91
+ "or run `public accounts set-default ACCOUNT_ID`."
92
+ )
93
+ return resolved
94
+
95
+
96
+ def _request_access_token(runtime: RuntimeConfig, secret: str) -> str:
97
+ client = ApiClient(base_url=runtime.base_url, token=None, timeout=runtime.timeout)
98
+ result = client.request(
99
+ "POST",
100
+ "/userapiauthservice/personal/access-tokens",
101
+ json_body={
102
+ "secret": secret,
103
+ "validityInMinutes": runtime.refresh_validity_minutes,
104
+ },
105
+ authenticated=False,
106
+ )
107
+ if not isinstance(result, dict) or not isinstance(result.get("accessToken"), str):
108
+ raise RuntimeError("Login response did not contain an accessToken.")
109
+ return result["accessToken"]
110
+
111
+
112
+ def _request_access_token_or_exit(runtime: RuntimeConfig, secret: str) -> str:
113
+ try:
114
+ return _request_access_token(runtime, secret)
115
+ except ApiError as exc:
116
+ print_error(str(exc))
117
+ if exc.body is not None:
118
+ print_json(exc.body)
119
+ raise typer.Exit(1) from exc
120
+ except RuntimeError as exc:
121
+ exit_with_error(str(exc))
122
+
123
+
124
+ def _refresh_token(ctx: typer.Context, *, force: bool = False) -> bool:
125
+ runtime = _runtime(ctx)
126
+ if not runtime.auto_refresh or not runtime.personal_secret:
127
+ return False
128
+ if not force and not token_expires_soon(
129
+ runtime.token, skew_seconds=runtime.refresh_skew_seconds
130
+ ):
131
+ return False
132
+
133
+ try:
134
+ token = _request_access_token(runtime, runtime.personal_secret)
135
+ except ApiError as exc:
136
+ print_error("Automatic token refresh failed.")
137
+ print_error(str(exc))
138
+ if exc.body is not None:
139
+ print_json(exc.body)
140
+ raise typer.Exit(1) from exc
141
+ except RuntimeError as exc:
142
+ exit_with_error(str(exc))
143
+
144
+ runtime.token = token
145
+ set_token(token)
146
+ return True
147
+
148
+
149
+ def _print(ctx: typer.Context, data: Any, *, table: str | None = None) -> None:
150
+ runtime = _runtime(ctx)
151
+ if runtime.json_output:
152
+ print_json(data)
153
+ elif table == "accounts":
154
+ print_accounts(data)
155
+ elif table == "quotes":
156
+ print_quotes(data)
157
+ else:
158
+ print_json(data)
159
+
160
+
161
+ def _call(ctx: typer.Context, method: str, path: str, **kwargs: Any) -> Any:
162
+ authenticated = bool(kwargs.get("authenticated", True))
163
+ if authenticated:
164
+ _refresh_token(ctx)
165
+
166
+ try:
167
+ return _api(ctx).request(method, path, **kwargs)
168
+ except MissingTokenError as exc:
169
+ if authenticated and _refresh_token(ctx, force=True):
170
+ return _api(ctx).request(method, path, **kwargs)
171
+ exit_with_error(str(exc))
172
+ except ApiError as exc:
173
+ if authenticated and exc.status_code == 401 and _refresh_token(ctx, force=True):
174
+ return _api(ctx).request(method, path, **kwargs)
175
+ print_error(str(exc))
176
+ if exc.body is not None:
177
+ print_json(exc.body)
178
+ raise typer.Exit(1) from exc
179
+
180
+
181
+ ORDER_ACTION_WARNING = (
182
+ "Trading action: review the account, symbols, side, quantity, prices, "
183
+ "expiration, time-in-force, and full request payload before continuing."
184
+ )
185
+
186
+
187
+ def _confirm(action: str, yes: bool, *, warning: str | None = None) -> None:
188
+ if yes:
189
+ return
190
+ if warning:
191
+ console.print(f"[yellow]{warning}[/yellow]")
192
+ if not typer.confirm(action):
193
+ raise typer.Abort()
194
+
195
+
196
+ def _version_callback(value: bool) -> None:
197
+ if value:
198
+ console.print(f"publicdotcom-cli {__version__}")
199
+ raise typer.Exit()
200
+
201
+
202
+ @app.callback()
203
+ def root(
204
+ ctx: typer.Context,
205
+ version: Annotated[
206
+ bool | None,
207
+ typer.Option(
208
+ "--version",
209
+ callback=_version_callback,
210
+ is_eager=True,
211
+ help="Show the CLI version and exit.",
212
+ ),
213
+ ] = None,
214
+ base_url: Annotated[
215
+ str,
216
+ typer.Option(
217
+ "--base-url",
218
+ envvar=BASE_URL_ENV_VAR,
219
+ help="API base URL.",
220
+ ),
221
+ ] = DEFAULT_BASE_URL,
222
+ token: Annotated[
223
+ str | None,
224
+ typer.Option(
225
+ "--token",
226
+ envvar=TOKEN_ENV_VAR,
227
+ help="Access token. Prefer PUBLIC_ACCESS_TOKEN for automation.",
228
+ ),
229
+ ] = None,
230
+ timeout: Annotated[
231
+ float,
232
+ typer.Option("--timeout", min=1.0, help="HTTP timeout in seconds."),
233
+ ] = 30.0,
234
+ auto_refresh: Annotated[
235
+ bool,
236
+ typer.Option(
237
+ "--auto-refresh/--no-auto-refresh",
238
+ envvar=AUTO_REFRESH_ENV_VAR,
239
+ help="Refresh access tokens from a stored or environment personal secret.",
240
+ ),
241
+ ] = True,
242
+ refresh_validity_minutes: Annotated[
243
+ int,
244
+ typer.Option(
245
+ "--refresh-validity-minutes",
246
+ min=5,
247
+ max=1440,
248
+ help="Lifetime for automatically refreshed access tokens.",
249
+ ),
250
+ ] = 60,
251
+ refresh_skew_seconds: Annotated[
252
+ int,
253
+ typer.Option(
254
+ "--refresh-skew-seconds",
255
+ min=0,
256
+ help="Refresh JWTs this many seconds before their exp timestamp.",
257
+ ),
258
+ ] = 60,
259
+ json_output: Annotated[
260
+ bool,
261
+ typer.Option("--json", help="Always print raw JSON output."),
262
+ ] = False,
263
+ ) -> None:
264
+ ctx.obj = RuntimeConfig(
265
+ base_url=base_url,
266
+ token=token or get_token(),
267
+ personal_secret=get_personal_secret(),
268
+ default_account_id=get_default_account_id(),
269
+ timeout=timeout,
270
+ json_output=json_output,
271
+ auto_refresh=auto_refresh,
272
+ refresh_validity_minutes=refresh_validity_minutes,
273
+ refresh_skew_seconds=refresh_skew_seconds,
274
+ )
275
+
276
+
277
+ @auth_app.command("login")
278
+ def auth_login(
279
+ ctx: typer.Context,
280
+ secret: Annotated[
281
+ str | None,
282
+ typer.Option("--secret", help="Personal secret. If omitted, you will be prompted."),
283
+ ] = None,
284
+ validity_minutes: Annotated[
285
+ int,
286
+ typer.Option(
287
+ "--validity-minutes",
288
+ min=5,
289
+ max=1440,
290
+ help="Access token lifetime in minutes.",
291
+ ),
292
+ ] = 60,
293
+ print_token: Annotated[
294
+ bool,
295
+ typer.Option("--print-token", help="Print the access token after login."),
296
+ ] = False,
297
+ store_secret: Annotated[
298
+ bool,
299
+ typer.Option(
300
+ "--store-secret",
301
+ help="Store the personal secret so future commands can refresh tokens automatically.",
302
+ ),
303
+ ] = False,
304
+ ) -> None:
305
+ secret = secret or getpass("Personal secret: ")
306
+ runtime = _runtime(ctx)
307
+ runtime.refresh_validity_minutes = validity_minutes
308
+ token = _request_access_token_or_exit(runtime, secret)
309
+ runtime.token = token
310
+ location = set_token(token)
311
+ console.print(f"Access token stored in {location}.")
312
+ if store_secret:
313
+ secret_location = set_personal_secret(secret)
314
+ runtime.personal_secret = secret
315
+ console.print(f"Personal secret stored in {secret_location} for automatic refresh.")
316
+ if print_token:
317
+ console.print(token)
318
+
319
+
320
+ @auth_app.command("refresh")
321
+ def auth_refresh(
322
+ ctx: typer.Context,
323
+ secret: Annotated[
324
+ str | None,
325
+ typer.Option("--secret", help="Personal secret. Uses stored secret if omitted."),
326
+ ] = None,
327
+ validity_minutes: Annotated[
328
+ int,
329
+ typer.Option(
330
+ "--validity-minutes",
331
+ min=5,
332
+ max=1440,
333
+ help="Access token lifetime in minutes.",
334
+ ),
335
+ ] = 60,
336
+ store_secret: Annotated[
337
+ bool,
338
+ typer.Option(
339
+ "--store-secret", help="Store the provided personal secret for future refresh."
340
+ ),
341
+ ] = False,
342
+ print_token: Annotated[
343
+ bool,
344
+ typer.Option("--print-token", help="Print the refreshed access token."),
345
+ ] = False,
346
+ ) -> None:
347
+ runtime = _runtime(ctx)
348
+ secret = secret or runtime.personal_secret or getpass("Personal secret: ")
349
+ runtime.refresh_validity_minutes = validity_minutes
350
+ token = _request_access_token_or_exit(runtime, secret)
351
+ runtime.token = token
352
+ location = set_token(token)
353
+ console.print(f"Access token refreshed and stored in {location}.")
354
+ if store_secret:
355
+ secret_location = set_personal_secret(secret)
356
+ runtime.personal_secret = secret
357
+ console.print(f"Personal secret stored in {secret_location} for automatic refresh.")
358
+ if print_token:
359
+ console.print(token)
360
+
361
+
362
+ @auth_app.command("status")
363
+ def auth_status(ctx: typer.Context) -> None:
364
+ runtime = _runtime(ctx)
365
+ console.print(f"Base URL: {runtime.base_url}")
366
+ console.print(f"Access token: {mask_token(runtime.token)}")
367
+ console.print(f"Personal secret: {mask_token(runtime.personal_secret)}")
368
+ console.print(f"Default account ID: {runtime.default_account_id or 'not set'}")
369
+ console.print(f"Auto refresh: {'enabled' if runtime.auto_refresh else 'disabled'}")
370
+ console.print(f"Secret env var: {SECRET_ENV_VAR}")
371
+ console.print(f"Account env var: {ACCOUNT_ID_ENV_VAR}")
372
+
373
+
374
+ @auth_app.command("logout")
375
+ def auth_logout(
376
+ clear_secret: Annotated[
377
+ bool,
378
+ typer.Option("--all", help="Also remove the stored personal secret."),
379
+ ] = False,
380
+ ) -> None:
381
+ clear_token()
382
+ console.print("Access token removed.")
383
+ if clear_secret:
384
+ clear_personal_secret()
385
+ console.print("Personal secret removed.")
386
+
387
+
388
+ @accounts_app.command("list")
389
+ def accounts_list(ctx: typer.Context) -> None:
390
+ result = _call(ctx, "GET", "/userapigateway/trading/account")
391
+ _print(ctx, result, table="accounts")
392
+
393
+
394
+ @accounts_app.command("set-default")
395
+ def accounts_set_default(
396
+ ctx: typer.Context,
397
+ account_id: Annotated[
398
+ str, typer.Argument(help="Account ID returned by `public accounts list`.")
399
+ ],
400
+ ) -> None:
401
+ runtime = _runtime(ctx)
402
+ runtime.default_account_id = account_id
403
+ location = set_default_account_id(account_id)
404
+ console.print(f"Default account ID set to {account_id} in {location}.")
405
+
406
+
407
+ @accounts_app.command("get-default")
408
+ def accounts_get_default(ctx: typer.Context) -> None:
409
+ runtime = _runtime(ctx)
410
+ if not runtime.default_account_id:
411
+ exit_with_error("No default account ID set. Run `public accounts set-default ACCOUNT_ID`.")
412
+ console.print(runtime.default_account_id)
413
+
414
+
415
+ @accounts_app.command("clear-default")
416
+ def accounts_clear_default(ctx: typer.Context) -> None:
417
+ runtime = _runtime(ctx)
418
+ runtime.default_account_id = None
419
+ clear_default_account_id()
420
+ console.print("Default account ID removed.")
421
+
422
+
423
+ @portfolio_app.command("show")
424
+ def portfolio_show(
425
+ ctx: typer.Context,
426
+ account_id: Annotated[
427
+ str | None,
428
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
429
+ ] = None,
430
+ ) -> None:
431
+ account_id = _resolve_account_id(ctx, account_id)
432
+ result = _call(ctx, "GET", f"/userapigateway/trading/{account_id}/portfolio/v2")
433
+ _print(ctx, result)
434
+
435
+
436
+ @history_app.command("list")
437
+ def history_list(
438
+ ctx: typer.Context,
439
+ account_id: Annotated[
440
+ str | None,
441
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
442
+ ] = None,
443
+ start: Annotated[str | None, typer.Option("--start", help="ISO 8601 start timestamp.")] = None,
444
+ end: Annotated[str | None, typer.Option("--end", help="ISO 8601 end timestamp.")] = None,
445
+ page_size: Annotated[int | None, typer.Option("--page-size", min=1)] = None,
446
+ next_token: Annotated[str | None, typer.Option("--next-token")] = None,
447
+ ) -> None:
448
+ account_id = _resolve_account_id(ctx, account_id)
449
+ result = _call(
450
+ ctx,
451
+ "GET",
452
+ f"/userapigateway/trading/{account_id}/history",
453
+ params={
454
+ "start": start,
455
+ "end": end,
456
+ "pageSize": page_size,
457
+ "nextToken": next_token,
458
+ },
459
+ )
460
+ _print(ctx, result)
461
+
462
+
463
+ @instruments_app.command("list")
464
+ def instruments_list(
465
+ ctx: typer.Context,
466
+ type_filter: Annotated[
467
+ list[str] | None,
468
+ typer.Option("--type-filter", help="Security type filter. Repeat for multiple."),
469
+ ] = None,
470
+ trading_filter: Annotated[
471
+ list[str] | None,
472
+ typer.Option("--trading-filter", help="Trading status filter. Repeat for multiple."),
473
+ ] = None,
474
+ fractional_trading_filter: Annotated[
475
+ list[str] | None,
476
+ typer.Option(
477
+ "--fractional-trading-filter",
478
+ help="Fractional trading status filter. Repeat for multiple.",
479
+ ),
480
+ ] = None,
481
+ option_trading_filter: Annotated[
482
+ list[str] | None,
483
+ typer.Option("--option-trading-filter", help="Option trading filter. Repeat for multiple."),
484
+ ] = None,
485
+ option_spread_trading_filter: Annotated[
486
+ list[str] | None,
487
+ typer.Option(
488
+ "--option-spread-trading-filter",
489
+ help="Option spread trading filter. Repeat for multiple.",
490
+ ),
491
+ ] = None,
492
+ ) -> None:
493
+ result = _call(
494
+ ctx,
495
+ "GET",
496
+ "/userapigateway/trading/instruments",
497
+ params={
498
+ "typeFilter": type_filter,
499
+ "tradingFilter": trading_filter,
500
+ "fractionalTradingFilter": fractional_trading_filter,
501
+ "optionTradingFilter": option_trading_filter,
502
+ "optionSpreadTradingFilter": option_spread_trading_filter,
503
+ },
504
+ )
505
+ _print(ctx, result)
506
+
507
+
508
+ @instruments_app.command("get")
509
+ def instrument_get(
510
+ ctx: typer.Context,
511
+ symbol: Annotated[str, typer.Argument()],
512
+ security_type: Annotated[str, typer.Argument(help="EQUITY, OPTION, CRYPTO, etc.")],
513
+ ) -> None:
514
+ result = _call(
515
+ ctx,
516
+ "GET",
517
+ f"/userapigateway/trading/instruments/{symbol.upper()}/{security_type.upper()}",
518
+ )
519
+ _print(ctx, result)
520
+
521
+
522
+ @market_app.command("quotes")
523
+ def market_quotes(
524
+ ctx: typer.Context,
525
+ symbols: Annotated[list[str], typer.Argument(help="One or more symbols.")],
526
+ account_id: Annotated[
527
+ str | None,
528
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
529
+ ] = None,
530
+ security_type: Annotated[
531
+ str,
532
+ typer.Option("--type", help="Instrument type for all symbols."),
533
+ ] = "EQUITY",
534
+ ) -> None:
535
+ account_id = _resolve_account_id(ctx, account_id)
536
+ result = _call(
537
+ ctx,
538
+ "POST",
539
+ f"/userapigateway/marketdata/{account_id}/quotes",
540
+ json_body={"instruments": instruments(symbols, security_type)},
541
+ )
542
+ _print(ctx, result, table="quotes")
543
+
544
+
545
+ @market_app.command("option-expirations")
546
+ def option_expirations(
547
+ ctx: typer.Context,
548
+ symbol: Annotated[str, typer.Argument()],
549
+ account_id: Annotated[
550
+ str | None,
551
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
552
+ ] = None,
553
+ security_type: Annotated[
554
+ str,
555
+ typer.Option("--type", help="Underlying instrument type."),
556
+ ] = "EQUITY",
557
+ ) -> None:
558
+ account_id = _resolve_account_id(ctx, account_id)
559
+ result = _call(
560
+ ctx,
561
+ "POST",
562
+ f"/userapigateway/marketdata/{account_id}/option-expirations",
563
+ json_body={"instrument": instrument(symbol, security_type)},
564
+ )
565
+ _print(ctx, result)
566
+
567
+
568
+ @market_app.command("option-chain")
569
+ def option_chain(
570
+ ctx: typer.Context,
571
+ symbol: Annotated[str, typer.Argument()],
572
+ expiration_date: Annotated[str, typer.Argument(help="Expiration date as YYYY-MM-DD.")],
573
+ account_id: Annotated[
574
+ str | None,
575
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
576
+ ] = None,
577
+ security_type: Annotated[
578
+ str,
579
+ typer.Option("--type", help="Underlying instrument type."),
580
+ ] = "EQUITY",
581
+ ) -> None:
582
+ account_id = _resolve_account_id(ctx, account_id)
583
+ result = _call(
584
+ ctx,
585
+ "POST",
586
+ f"/userapigateway/marketdata/{account_id}/option-chain",
587
+ json_body={
588
+ "instrument": instrument(symbol, security_type),
589
+ "expirationDate": expiration_date,
590
+ },
591
+ )
592
+ _print(ctx, result)
593
+
594
+
595
+ @options_app.command("greeks")
596
+ def option_greeks(
597
+ ctx: typer.Context,
598
+ osi_symbols: Annotated[list[str], typer.Argument(help="One or more OSI-normalized symbols.")],
599
+ account_id: Annotated[
600
+ str | None,
601
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
602
+ ] = None,
603
+ ) -> None:
604
+ account_id = _resolve_account_id(ctx, account_id)
605
+ result = _call(
606
+ ctx,
607
+ "GET",
608
+ f"/userapigateway/option-details/{account_id}/greeks",
609
+ params={"osiSymbols": osi_symbols},
610
+ )
611
+ _print(ctx, result)
612
+
613
+
614
+ @order_app.command("preflight-single")
615
+ def preflight_single(
616
+ ctx: typer.Context,
617
+ file: Annotated[Path, typer.Option("--file", "-f", exists=True, readable=True)],
618
+ account_id: Annotated[
619
+ str | None,
620
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
621
+ ] = None,
622
+ ) -> None:
623
+ account_id = _resolve_account_id(ctx, account_id)
624
+ body = load_json_file(file)
625
+ result = _call(
626
+ ctx,
627
+ "POST",
628
+ f"/userapigateway/trading/{account_id}/preflight/single-leg",
629
+ json_body=body,
630
+ )
631
+ _print(ctx, result)
632
+
633
+
634
+ @order_app.command("preflight-multi")
635
+ def preflight_multi(
636
+ ctx: typer.Context,
637
+ file: Annotated[Path, typer.Option("--file", "-f", exists=True, readable=True)],
638
+ account_id: Annotated[
639
+ str | None,
640
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
641
+ ] = None,
642
+ ) -> None:
643
+ account_id = _resolve_account_id(ctx, account_id)
644
+ body = load_json_file(file)
645
+ result = _call(
646
+ ctx,
647
+ "POST",
648
+ f"/userapigateway/trading/{account_id}/preflight/multi-leg",
649
+ json_body=body,
650
+ )
651
+ _print(ctx, result)
652
+
653
+
654
+ @order_app.command("place")
655
+ def order_place(
656
+ ctx: typer.Context,
657
+ file: Annotated[Path, typer.Option("--file", "-f", exists=True, readable=True)],
658
+ account_id: Annotated[
659
+ str | None,
660
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
661
+ ] = None,
662
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation.")] = False,
663
+ ) -> None:
664
+ account_id = _resolve_account_id(ctx, account_id)
665
+ body = load_json_file(file)
666
+ if not isinstance(body, dict):
667
+ exit_with_error("Order request JSON must be an object.")
668
+ order_id = ensure_order_id(body)
669
+ _confirm(f"Submit order {order_id}?", yes, warning=ORDER_ACTION_WARNING)
670
+ result = _call(
671
+ ctx,
672
+ "POST",
673
+ f"/userapigateway/trading/{account_id}/order",
674
+ json_body=body,
675
+ )
676
+ _print(ctx, result)
677
+
678
+
679
+ @order_app.command("replace")
680
+ def order_replace(
681
+ ctx: typer.Context,
682
+ file: Annotated[Path, typer.Option("--file", "-f", exists=True, readable=True)],
683
+ account_id: Annotated[
684
+ str | None,
685
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
686
+ ] = None,
687
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation.")] = False,
688
+ ) -> None:
689
+ account_id = _resolve_account_id(ctx, account_id)
690
+ body = load_json_file(file)
691
+ _confirm("Submit cancel-replace request?", yes, warning=ORDER_ACTION_WARNING)
692
+ result = _call(
693
+ ctx,
694
+ "PUT",
695
+ f"/userapigateway/trading/{account_id}/order",
696
+ json_body=body,
697
+ )
698
+ _print(ctx, result)
699
+
700
+
701
+ @order_app.command("place-multileg")
702
+ def order_place_multileg(
703
+ ctx: typer.Context,
704
+ file: Annotated[Path, typer.Option("--file", "-f", exists=True, readable=True)],
705
+ account_id: Annotated[
706
+ str | None,
707
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
708
+ ] = None,
709
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation.")] = False,
710
+ ) -> None:
711
+ account_id = _resolve_account_id(ctx, account_id)
712
+ body = load_json_file(file)
713
+ if not isinstance(body, dict):
714
+ exit_with_error("Multileg order request JSON must be an object.")
715
+ order_id = ensure_order_id(body)
716
+ _confirm(f"Submit multileg order {order_id}?", yes, warning=ORDER_ACTION_WARNING)
717
+ result = _call(
718
+ ctx,
719
+ "POST",
720
+ f"/userapigateway/trading/{account_id}/order/multileg",
721
+ json_body=body,
722
+ )
723
+ _print(ctx, result)
724
+
725
+
726
+ @order_app.command("get")
727
+ def order_get(
728
+ ctx: typer.Context,
729
+ order_id: Annotated[str, typer.Argument()],
730
+ account_id: Annotated[
731
+ str | None,
732
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
733
+ ] = None,
734
+ ) -> None:
735
+ account_id = _resolve_account_id(ctx, account_id)
736
+ result = _call(ctx, "GET", f"/userapigateway/trading/{account_id}/order/{order_id}")
737
+ _print(ctx, result)
738
+
739
+
740
+ @order_app.command("cancel")
741
+ def order_cancel(
742
+ ctx: typer.Context,
743
+ order_id: Annotated[str, typer.Argument()],
744
+ account_id: Annotated[
745
+ str | None,
746
+ typer.Option("--account-id", "-a", help="Account ID. Defaults to configured account."),
747
+ ] = None,
748
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation.")] = False,
749
+ ) -> None:
750
+ account_id = _resolve_account_id(ctx, account_id)
751
+ _confirm(
752
+ f"Cancel order {order_id}?",
753
+ yes,
754
+ warning="Trading action: cancellation requests may be asynchronous. Verify order status after submitting.",
755
+ )
756
+ result = _call(ctx, "DELETE", f"/userapigateway/trading/{account_id}/order/{order_id}")
757
+ _print(ctx, result if result is not None else {"cancelRequested": True})
758
+
759
+
760
+ def main() -> None:
761
+ app()