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,83 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+
9
+ class MissingTokenError(RuntimeError):
10
+ """Raised when a secured API endpoint is called without an access token."""
11
+
12
+
13
+ @dataclass
14
+ class ApiError(RuntimeError):
15
+ method: str
16
+ path: str
17
+ status_code: int
18
+ body: Any
19
+
20
+ def __str__(self) -> str:
21
+ return f"{self.method.upper()} {self.path} failed with HTTP {self.status_code}"
22
+
23
+
24
+ def _response_body(response: httpx.Response) -> Any:
25
+ if not response.content:
26
+ return None
27
+ try:
28
+ return response.json()
29
+ except ValueError:
30
+ return response.text
31
+
32
+
33
+ def compact_params(params: dict[str, Any] | None) -> list[tuple[str, Any]]:
34
+ if not params:
35
+ return []
36
+
37
+ compacted: list[tuple[str, Any]] = []
38
+ for key, value in params.items():
39
+ if value is None or value == []:
40
+ continue
41
+ if isinstance(value, list):
42
+ compacted.extend((key, item) for item in value)
43
+ else:
44
+ compacted.append((key, value))
45
+ return compacted
46
+
47
+
48
+ class ApiClient:
49
+ def __init__(self, *, base_url: str, token: str | None, timeout: float) -> None:
50
+ self.base_url = base_url.rstrip("/")
51
+ self.token = token
52
+ self.timeout = timeout
53
+
54
+ def request(
55
+ self,
56
+ method: str,
57
+ path: str,
58
+ *,
59
+ params: dict[str, Any] | None = None,
60
+ json_body: Any | None = None,
61
+ authenticated: bool = True,
62
+ ) -> Any:
63
+ headers = {"Accept": "application/json"}
64
+ if authenticated:
65
+ if not self.token:
66
+ raise MissingTokenError(
67
+ "No access token found. Run `public auth login` or set PUBLIC_ACCESS_TOKEN."
68
+ )
69
+ headers["Authorization"] = f"Bearer {self.token}"
70
+
71
+ with httpx.Client(base_url=self.base_url, timeout=self.timeout) as client:
72
+ response = client.request(
73
+ method,
74
+ path,
75
+ params=compact_params(params),
76
+ json=json_body,
77
+ headers=headers,
78
+ )
79
+
80
+ body = _response_body(response)
81
+ if response.status_code >= 400:
82
+ raise ApiError(method=method, path=path, status_code=response.status_code, body=body)
83
+ return body
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from base64 import urlsafe_b64decode
7
+ from binascii import Error as BinasciiError
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import keyring
12
+ from keyring.errors import KeyringError
13
+ from platformdirs import user_config_dir
14
+
15
+ SERVICE_NAME = "publicdotcom-cli"
16
+ TOKEN_USERNAME = "access-token"
17
+ SECRET_USERNAME = "personal-secret"
18
+ TOKEN_ENV_VAR = "PUBLIC_ACCESS_TOKEN"
19
+ SECRET_ENV_VAR = "PUBLIC_PERSONAL_SECRET"
20
+ ACCOUNT_ID_ENV_VAR = "PUBLIC_ACCOUNT_ID"
21
+ BASE_URL_ENV_VAR = "PUBLIC_API_BASE_URL"
22
+ AUTO_REFRESH_ENV_VAR = "PUBLIC_AUTO_REFRESH"
23
+ DEFAULT_BASE_URL = "https://api.public.com"
24
+
25
+
26
+ def token_file() -> Path:
27
+ return Path(user_config_dir("publicdotcom-cli", "publicdotcom")) / "token.json"
28
+
29
+
30
+ def settings_file() -> Path:
31
+ return Path(user_config_dir("publicdotcom-cli", "publicdotcom")) / "settings.json"
32
+
33
+
34
+ def _read_settings() -> dict[str, Any]:
35
+ path = settings_file()
36
+ if not path.exists():
37
+ return {}
38
+
39
+ try:
40
+ data = json.loads(path.read_text(encoding="utf-8"))
41
+ except (OSError, json.JSONDecodeError):
42
+ return {}
43
+
44
+ return data if isinstance(data, dict) else {}
45
+
46
+
47
+ def _write_settings(settings: dict[str, Any]) -> str:
48
+ path = settings_file()
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
51
+ path.chmod(0o600)
52
+ return str(path)
53
+
54
+
55
+ def get_token() -> str | None:
56
+ env_token = os.getenv(TOKEN_ENV_VAR)
57
+ if env_token:
58
+ return env_token
59
+
60
+ try:
61
+ token = keyring.get_password(SERVICE_NAME, TOKEN_USERNAME)
62
+ except (KeyringError, RuntimeError, ImportError):
63
+ token = None
64
+
65
+ if token:
66
+ return token
67
+
68
+ path = token_file()
69
+ if not path.exists():
70
+ return None
71
+
72
+ try:
73
+ data = json.loads(path.read_text(encoding="utf-8"))
74
+ except (OSError, json.JSONDecodeError):
75
+ return None
76
+
77
+ token = data.get("accessToken")
78
+ return token if isinstance(token, str) and token else None
79
+
80
+
81
+ def get_personal_secret() -> str | None:
82
+ env_secret = os.getenv(SECRET_ENV_VAR)
83
+ if env_secret:
84
+ return env_secret
85
+
86
+ try:
87
+ secret = keyring.get_password(SERVICE_NAME, SECRET_USERNAME)
88
+ except (KeyringError, RuntimeError, ImportError):
89
+ secret = None
90
+
91
+ if secret:
92
+ return secret
93
+
94
+ path = token_file().with_name("secret.json")
95
+ if not path.exists():
96
+ return None
97
+
98
+ try:
99
+ data = json.loads(path.read_text(encoding="utf-8"))
100
+ except (OSError, json.JSONDecodeError):
101
+ return None
102
+
103
+ secret = data.get("personalSecret")
104
+ return secret if isinstance(secret, str) and secret else None
105
+
106
+
107
+ def get_default_account_id() -> str | None:
108
+ env_account_id = os.getenv(ACCOUNT_ID_ENV_VAR)
109
+ if env_account_id:
110
+ return env_account_id
111
+
112
+ account_id = _read_settings().get("defaultAccountId")
113
+ return account_id if isinstance(account_id, str) and account_id else None
114
+
115
+
116
+ def set_token(token: str) -> str:
117
+ try:
118
+ keyring.set_password(SERVICE_NAME, TOKEN_USERNAME, token)
119
+ return "keyring"
120
+ except (KeyringError, RuntimeError, ImportError):
121
+ path = token_file()
122
+ path.parent.mkdir(parents=True, exist_ok=True)
123
+ path.write_text(json.dumps({"accessToken": token}, indent=2), encoding="utf-8")
124
+ path.chmod(0o600)
125
+ return str(path)
126
+
127
+
128
+ def set_personal_secret(secret: str) -> str:
129
+ try:
130
+ keyring.set_password(SERVICE_NAME, SECRET_USERNAME, secret)
131
+ return "keyring"
132
+ except (KeyringError, RuntimeError, ImportError):
133
+ path = token_file().with_name("secret.json")
134
+ path.parent.mkdir(parents=True, exist_ok=True)
135
+ path.write_text(json.dumps({"personalSecret": secret}, indent=2), encoding="utf-8")
136
+ path.chmod(0o600)
137
+ return str(path)
138
+
139
+
140
+ def set_default_account_id(account_id: str) -> str:
141
+ settings = _read_settings()
142
+ settings["defaultAccountId"] = account_id
143
+ return _write_settings(settings)
144
+
145
+
146
+ def clear_token() -> None:
147
+ try:
148
+ keyring.delete_password(SERVICE_NAME, TOKEN_USERNAME)
149
+ except (KeyringError, RuntimeError, ImportError, keyring.errors.PasswordDeleteError):
150
+ pass
151
+
152
+ path = token_file()
153
+ try:
154
+ path.unlink()
155
+ except FileNotFoundError:
156
+ pass
157
+
158
+
159
+ def clear_personal_secret() -> None:
160
+ try:
161
+ keyring.delete_password(SERVICE_NAME, SECRET_USERNAME)
162
+ except (KeyringError, RuntimeError, ImportError, keyring.errors.PasswordDeleteError):
163
+ pass
164
+
165
+ path = token_file().with_name("secret.json")
166
+ try:
167
+ path.unlink()
168
+ except FileNotFoundError:
169
+ pass
170
+
171
+
172
+ def clear_default_account_id() -> None:
173
+ settings = _read_settings()
174
+ settings.pop("defaultAccountId", None)
175
+ path = settings_file()
176
+ if settings:
177
+ _write_settings(settings)
178
+ return
179
+
180
+ try:
181
+ path.unlink()
182
+ except FileNotFoundError:
183
+ pass
184
+
185
+
186
+ def mask_token(token: str | None) -> str:
187
+ if not token:
188
+ return "not set"
189
+ if len(token) <= 12:
190
+ return "*" * len(token)
191
+ return f"{token[:6]}...{token[-6:]}"
192
+
193
+
194
+ def _decode_jwt_payload(token: str) -> dict[str, Any] | None:
195
+ parts = token.split(".")
196
+ if len(parts) < 2:
197
+ return None
198
+
199
+ payload = parts[1]
200
+ payload += "=" * (-len(payload) % 4)
201
+ try:
202
+ decoded = urlsafe_b64decode(payload.encode("ascii"))
203
+ data = json.loads(decoded.decode("utf-8"))
204
+ except (BinasciiError, ValueError, UnicodeDecodeError):
205
+ return None
206
+
207
+ return data if isinstance(data, dict) else None
208
+
209
+
210
+ def token_expires_soon(token: str | None, *, skew_seconds: int = 60) -> bool:
211
+ if not token:
212
+ return True
213
+
214
+ payload = _decode_jwt_payload(token)
215
+ if not payload:
216
+ return False
217
+
218
+ expires_at = payload.get("exp")
219
+ if not isinstance(expires_at, int | float):
220
+ return False
221
+
222
+ return expires_at <= time.time() + skew_seconds
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ console = Console()
11
+ err_console = Console(stderr=True)
12
+
13
+
14
+ def print_json(data: Any) -> None:
15
+ console.print_json(json.dumps(data, default=str))
16
+
17
+
18
+ def print_error(message: str) -> None:
19
+ err_console.print(f"[red]{message}[/red]")
20
+
21
+
22
+ def print_accounts(data: Any) -> None:
23
+ accounts = data.get("accounts") if isinstance(data, dict) else None
24
+ if not accounts:
25
+ print_json(data)
26
+ return
27
+
28
+ table = Table(title="Accounts")
29
+ table.add_column("Account ID")
30
+ table.add_column("Type")
31
+ table.add_column("Brokerage")
32
+ table.add_column("Options")
33
+ table.add_column("Permissions")
34
+
35
+ for account in accounts:
36
+ table.add_row(
37
+ str(account.get("accountId", "")),
38
+ str(account.get("accountType", "")),
39
+ str(account.get("brokerageAccountType", "")),
40
+ str(account.get("optionsLevel", "")),
41
+ str(account.get("tradePermissions", "")),
42
+ )
43
+ console.print(table)
44
+
45
+
46
+ def print_quotes(data: Any) -> None:
47
+ quotes = data.get("quotes") if isinstance(data, dict) else None
48
+ if not quotes:
49
+ print_json(data)
50
+ return
51
+
52
+ table = Table(title="Quotes")
53
+ table.add_column("Symbol")
54
+ table.add_column("Type")
55
+ table.add_column("Outcome")
56
+ table.add_column("Last", justify="right")
57
+ table.add_column("Bid", justify="right")
58
+ table.add_column("Ask", justify="right")
59
+ table.add_column("Volume", justify="right")
60
+
61
+ for quote in quotes:
62
+ instrument = quote.get("instrument") or {}
63
+ table.add_row(
64
+ str(instrument.get("symbol", "")),
65
+ str(instrument.get("type", "")),
66
+ str(quote.get("outcome", "")),
67
+ str(quote.get("last", "")),
68
+ str(quote.get("bid", "")),
69
+ str(quote.get("ask", "")),
70
+ str(quote.get("volume", "")),
71
+ )
72
+ console.print(table)
73
+
74
+
75
+ def exit_with_error(message: str, code: int = 1) -> None:
76
+ print_error(message)
77
+ raise typer.Exit(code)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ import uuid
6
+ from pathlib import Path
7
+ from typing import Any, Iterable
8
+
9
+
10
+ def load_json_file(path: Path) -> Any:
11
+ if str(path) == "-":
12
+ raw = sys.stdin.read()
13
+ else:
14
+ raw = path.read_text(encoding="utf-8")
15
+
16
+ try:
17
+ return json.loads(raw)
18
+ except json.JSONDecodeError as exc:
19
+ raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
20
+
21
+
22
+ def instrument(symbol: str, security_type: str) -> dict[str, str]:
23
+ return {"symbol": symbol.upper(), "type": security_type.upper()}
24
+
25
+
26
+ def instruments(symbols: Iterable[str], security_type: str) -> list[dict[str, str]]:
27
+ return [instrument(symbol, security_type) for symbol in symbols]
28
+
29
+
30
+ def ensure_order_id(body: dict[str, Any]) -> str:
31
+ order_id = body.get("orderId")
32
+ if isinstance(order_id, str) and order_id:
33
+ return order_id
34
+ order_id = str(uuid.uuid4())
35
+ body["orderId"] = order_id
36
+ return order_id
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: publicdotcom-cli
3
+ Version: 1.0.0
4
+ Summary: Command-line client for the Public.com Trading API
5
+ Project-URL: Homepage, https://github.com/publicdotcom/publicdotcom-cli
6
+ Project-URL: Repository, https://github.com/publicdotcom/publicdotcom-cli
7
+ Project-URL: Issues, https://github.com/publicdotcom/publicdotcom-cli/issues
8
+ Project-URL: Public.com API, https://public.com/api
9
+ Author-email: "Public.com" <developers@public.com>
10
+ Maintainer-email: "Public.com" <developers@public.com>
11
+ License-Expression: Apache-2.0
12
+ Keywords: api,brokerage,cli,crypto,options,public.com,stocks,trading
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial :: Investment
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: attrs>=23.2.0
27
+ Requires-Dist: httpx>=0.28.0
28
+ Requires-Dist: keyring>=25.0.0
29
+ Requires-Dist: platformdirs>=4.0.0
30
+ Requires-Dist: python-dateutil>=2.8.2
31
+ Requires-Dist: rich>=13.7.0
32
+ Requires-Dist: typer>=0.16.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.11.0; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # publicdotcom-cli
39
+
40
+ Command-line client for the Public.com Trading API.
41
+
42
+ Use `public` to authenticate, inspect accounts, retrieve portfolio and market data, run
43
+ preflight checks, and submit order-related requests from your terminal.
44
+
45
+ ## Install
46
+
47
+ The recommended installation method for command-line Python tools is `pipx`:
48
+
49
+ ```bash
50
+ pipx install publicdotcom-cli
51
+ ```
52
+
53
+ You can also install with `uv`:
54
+
55
+ ```bash
56
+ uv tool install publicdotcom-cli
57
+ ```
58
+
59
+ Confirm the CLI is available:
60
+
61
+ ```bash
62
+ public --help
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ Generate a personal secret from your Public.com settings, then authenticate:
68
+
69
+ ```bash
70
+ public auth login
71
+ public accounts list
72
+ public accounts set-default ACCOUNT_ID
73
+ public portfolio show
74
+ public market quotes AAPL MSFT
75
+ ```
76
+
77
+ Most account-scoped commands use the configured default account. You can override it with
78
+ `--account-id ACCOUNT_ID` or `PUBLIC_ACCOUNT_ID=ACCOUNT_ID`.
79
+
80
+ ## Important Disclosures
81
+
82
+ This CLI is a developer tool for interacting with the Public API. It is not investment,
83
+ financial, legal, tax, accounting, or trading advice, and it does not recommend any
84
+ security, strategy, account type, order type, or transaction.
85
+
86
+ Trading involves risk, including the possible loss of principal. You are responsible for
87
+ reviewing all request payloads, account IDs, symbols, quantities, prices, order sides,
88
+ time-in-force values, and other order instructions before submitting a trading command.
89
+
90
+ Order placement, replacement, and cancellation requests may be asynchronous. A successful
91
+ API response confirms submission to the API, not execution, cancellation, fill price,
92
+ availability, or final order state. Always verify order status after submitting,
93
+ replacing, or cancelling an order.
94
+
95
+ Market data, quotes, option chains, greeks, account data, and preflight calculations are
96
+ provided for informational and operational use through the API. They may be incomplete,
97
+ delayed, unavailable, or different from final execution values.
98
+
99
+ You are responsible for complying with all applicable laws, regulations, exchange rules,
100
+ API terms, account agreements, and internal policies that apply to your use of this CLI.
101
+ Do not use this tool unless you are authorized to access the relevant account and API
102
+ credentials.
103
+
104
+ Personal secrets and access tokens can authorize account access and trading activity.
105
+ Keep them private, do not commit them to source control, and rotate or revoke them if
106
+ you believe they were exposed.
107
+
108
+ ## Authenticate
109
+
110
+ Generate a personal secret from Public, then run:
111
+
112
+ ```bash
113
+ public auth login
114
+ ```
115
+
116
+ The access token is stored with your OS keychain when available. If no keychain backend
117
+ is available, the CLI falls back to a user-only config file.
118
+
119
+ Access tokens are short-lived. To let the CLI refresh them automatically, opt in to
120
+ storing your personal secret:
121
+
122
+ ```bash
123
+ public auth login --store-secret
124
+ ```
125
+
126
+ Because the personal secret is long-lived, this is optional. The CLI stores it in your
127
+ OS keychain when available, otherwise it falls back to a user-only config file.
128
+
129
+ After that, secured commands automatically mint a fresh access token before the current
130
+ token expires or after a `401 Unauthorized` response. You can also refresh manually:
131
+
132
+ ```bash
133
+ public auth refresh
134
+ ```
135
+
136
+ You can also bypass stored credentials for automation:
137
+
138
+ ```bash
139
+ PUBLIC_ACCESS_TOKEN=ey... public accounts list
140
+ PUBLIC_PERSONAL_SECRET=... public accounts list
141
+ ```
142
+
143
+ Remove stored credentials with:
144
+
145
+ ```bash
146
+ public auth logout
147
+ public auth logout --all
148
+ ```
149
+
150
+ ## Default Account
151
+
152
+ Most API operations require the `accountId` returned by `public accounts list`. You can
153
+ store a default account once:
154
+
155
+ ```bash
156
+ public accounts set-default ACCOUNT_ID
157
+ public accounts get-default
158
+ ```
159
+
160
+ Then omit `--account-id` from account-scoped commands:
161
+
162
+ ```bash
163
+ public portfolio show
164
+ public market quotes AAPL MSFT
165
+ public order get ORDER_ID
166
+ ```
167
+
168
+ You can override the default at any time:
169
+
170
+ ```bash
171
+ public portfolio show --account-id ACCOUNT_ID
172
+ PUBLIC_ACCOUNT_ID=ACCOUNT_ID public portfolio show
173
+ public accounts clear-default
174
+ ```
175
+
176
+ ## Example Commands
177
+
178
+ ```bash
179
+ public accounts list
180
+ public accounts set-default ACCOUNT_ID
181
+ public portfolio show
182
+ public history list --page-size 25
183
+ public instruments get AAPL EQUITY
184
+ public market quotes AAPL MSFT --type EQUITY
185
+ public market option-expirations AAPL
186
+ public market option-chain AAPL 2026-05-15
187
+ public options greeks "AAPL 260515C00200000"
188
+ ```
189
+
190
+ Trading requests use JSON files so the exact payload is visible before submission:
191
+
192
+ ```bash
193
+ public order preflight-single --file examples/order.single-leg.market-buy.json
194
+ public order place --file examples/order.single-leg.market-buy.json
195
+ public order get ORDER_ID
196
+ public order cancel ORDER_ID
197
+ ```
198
+
199
+ Trading commands prompt before submitting order placement, replacement, or cancellation
200
+ requests. Use `--yes` only when your automation has already performed equivalent
201
+ validation and approval.
202
+
203
+ ## JSON Output
204
+
205
+ Use `--json` before the command group to print raw JSON:
206
+
207
+ ```bash
208
+ public --json accounts list
209
+ public --json market quotes AAPL MSFT
210
+ ```
211
+
212
+ ## Configuration
213
+
214
+ The CLI supports these environment variables:
215
+
216
+ ```bash
217
+ PUBLIC_ACCESS_TOKEN=...
218
+ PUBLIC_PERSONAL_SECRET=...
219
+ PUBLIC_ACCOUNT_ID=...
220
+ PUBLIC_API_BASE_URL=https://api.public.com
221
+ PUBLIC_AUTO_REFRESH=true
222
+ ```
223
+
224
+ `PUBLIC_API_BASE_URL` is optional and defaults to `https://api.public.com`.
225
+
226
+ ## Upgrade
227
+
228
+ ```bash
229
+ pipx upgrade publicdotcom-cli
230
+ # or
231
+ uv tool upgrade publicdotcom-cli
232
+ ```
233
+
234
+ ## Development
235
+
236
+ For local development from a checkout:
237
+
238
+ ```bash
239
+ uv sync --extra dev
240
+ uv run public --help
241
+ uv run pytest
242
+ ```
243
+
244
+ ## Regenerate The OpenAPI Client
245
+
246
+ The package ships with a generated API client. Contributors who need to regenerate it
247
+ must place the local OpenAPI spec at the repository root as `spec.yaml`, then run:
248
+
249
+ ```bash
250
+ uv run python scripts/generate_client.py
251
+ ```
252
+
253
+ The raw spec uses `*/*` for many JSON responses, which some Python generators do not
254
+ parse as JSON. The regeneration script normalizes those response content types before
255
+ running `openapi-python-client`. This requires network access the first time because it
256
+ uses `uvx openapi-python-client`.