orionapi 2.27.0__tar.gz → 2.28.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionapi
3
- Version: 2.27.0
3
+ Version: 2.28.0
4
4
  Summary: A python interface for the Orion Advisors platform web API
5
5
  Author: spencerogden-dsam
6
6
  Author-email: 67068943+spencerogden-dsam@users.noreply.github.com
@@ -1,4 +1,4 @@
1
- __version__ = "2.27.0"
1
+ __version__ = "2.28.0"
2
2
 
3
3
  import logging
4
4
  import re
@@ -56,10 +56,16 @@ TRADE_INSTANCE_SUBTYPES = {
56
56
  }
57
57
 
58
58
  # TradingApplication mappings for the v2 /TradeOrder/Trades endpoints.
59
- # Not published in the Swagger (which lists the ints with no names) -- recovered
60
- # 2026-07-28 from the API's own rejection messages ("Trade Application <Name> is
61
- # not supported."). Only MANUAL_TRADE (11) actually creates trade orders; see
59
+ # Not published in the Swagger (which lists the ints with no names). Names 0-10
60
+ # and 12-18 were recovered 2026-07-28 from the API's own rejection messages
61
+ # ("Trade Application <Name> is not supported."); 11 is QuickTrades, confirmed
62
+ # 2026-08-03 by Orion support and verified live (the string "QuickTrades" and the
63
+ # int 11 return identical results). Only 11 creates trade orders -- see
62
64
  # EclipseV2.create_trades.
65
+ #
66
+ # These names are also accepted as *strings* in the ``application`` field, e.g.
67
+ # ``{"application": "QuickTrades"}``. A name outside this set is rejected with
68
+ # 400 "Error converting value ... to type 'OAS.Eclipse.Core.Enums.TradingApplication'".
63
69
  TRADING_APPLICATIONS = {
64
70
  0: "BuySell",
65
71
  1: "CashNeeds",
@@ -72,7 +78,7 @@ TRADING_APPLICATIONS = {
72
78
  8: "TaxLossHarvesting",
73
79
  9: "TickerSwap",
74
80
  10: "TradeToTargetPercent",
75
- 11: "ManualTrade",
81
+ 11: "QuickTrades",
76
82
  12: "TacticalTrades",
77
83
  13: "PartialShare",
78
84
  14: "(unnamed, accepted but creates nothing)",
@@ -86,7 +92,12 @@ TRADING_APPLICATIONS = {
86
92
  # POST /api/v2/TradeOrder/Trades. Every other value except 14 returns
87
93
  # 400 "Trade Application <Name> is not supported."; 14 returns 200 with an
88
94
  # empty ``tradeId`` list and creates nothing. Live-verified 2026-07-28.
89
- TRADING_APPLICATION_MANUAL = 11
95
+ TRADING_APPLICATION_QUICK_TRADES = 11
96
+
97
+ # Deprecated alias. 2.26.0 shipped this as TRADING_APPLICATION_MANUAL on the
98
+ # mistaken belief that 11 was named "ManualTrade"; the API rejects that name.
99
+ # Kept so 2.26.0/2.27.0 callers keep working -- the value is unchanged.
100
+ TRADING_APPLICATION_MANUAL = TRADING_APPLICATION_QUICK_TRADES
90
101
 
91
102
  # Set-Aside Cash Constants (from Eclipse API)
92
103
  CASH_TYPE_DOLLAR = 1
@@ -203,6 +214,17 @@ class NotFoundError(OrionAPIError):
203
214
  pass
204
215
 
205
216
 
217
+ class AmbiguousAccountError(OrionAPIError):
218
+ """Raised when an account search matches more than one account.
219
+
220
+ The message lists the candidate accounts (id, name, accountNumber) so the
221
+ caller can retry with an exact internal id, a full account number, or the
222
+ deliberate fuzzy workflow (``search_accounts_number_and_name``).
223
+ """
224
+
225
+ pass
226
+
227
+
206
228
  class RateLimiter:
207
229
  """Thread-safe rate limiter to prevent API abuse.
208
230
 
@@ -2348,30 +2370,71 @@ class EclipseBase(BaseAPI):
2348
2370
  res = self.api_request(f"{self.base_url}/account/accounts/simple")
2349
2371
  return res.json()
2350
2372
 
2351
- def get_internal_account_id(self, search_param):
2373
+ def get_internal_account_id(self, search_param, fuzzy=False):
2352
2374
  """Get internal Eclipse account ID from a search parameter.
2353
2375
 
2354
- Searches across id, accountName, accountNumber, and portfolioName.
2355
- Best use is to pass a full custodian account number.
2376
+ Searches across id, accountName, accountNumber, and portfolioName,
2377
+ then resolves in tiers:
2378
+
2379
+ 1. **Exact match:** a result whose internal ``id`` or ``accountNumber``
2380
+ equals ``search_param`` is returned immediately. If an exact-id and
2381
+ an exact-account-number match point at *different* accounts, raises
2382
+ ``AmbiguousAccountError`` rather than guessing.
2383
+ 2. A single remaining candidate is returned.
2384
+ 3. Multiple candidates raise ``AmbiguousAccountError`` listing them,
2385
+ unless ``fuzzy=True``, which returns the first match (the pre-2.28
2386
+ behavior).
2387
+
2388
+ Best use is to pass a full custodian account number. Callers that
2389
+ already hold the internal id should pass it via the
2390
+ ``internal_account_id=`` keyword on scoped methods instead of
2391
+ resolving here. For deliberate partial-number + name resolution use
2392
+ :meth:`search_accounts_number_and_name`.
2356
2393
 
2357
2394
  Args:
2358
2395
  search_param: Account number, name, or ID to search for
2396
+ fuzzy: When True, silently return the first match when multiple
2397
+ candidates remain (default False)
2359
2398
 
2360
2399
  Returns:
2361
2400
  int: The internal Eclipse account ID
2362
2401
 
2363
2402
  Raises:
2364
2403
  NotFoundError: If no matching account is found
2365
-
2366
- Note:
2367
- Returns the first matching result, which may not be expected
2368
- if multiple accounts match the search parameter.
2404
+ AmbiguousAccountError: If multiple accounts match and no exact
2405
+ match singles one out (and ``fuzzy`` is False)
2369
2406
  """
2370
2407
  res = self.search_accounts(search_param)
2371
2408
  logging.debug("search_accounts result: %s", res)
2372
2409
  if not res:
2373
2410
  raise NotFoundError(f"No account found for search: {search_param}")
2374
- return res[0]["id"]
2411
+
2412
+ def _candidates(accounts):
2413
+ return [{k: a.get(k) for k in ("id", "name", "accountNumber")} for a in accounts]
2414
+
2415
+ search_str = str(search_param)
2416
+ id_exact = [a for a in res if str(a.get("id")) == search_str]
2417
+ number_exact = [a for a in res if a.get("accountNumber") == search_str]
2418
+ exact = id_exact + [a for a in number_exact if a not in id_exact]
2419
+ if len(exact) == 1:
2420
+ return exact[0]["id"]
2421
+ if len(exact) > 1:
2422
+ raise AmbiguousAccountError(
2423
+ f"Search '{search_param}' exactly matches both an internal id and "
2424
+ f"other account numbers; pass internal_account_id= or a full "
2425
+ f"account number. Candidates: {_candidates(exact)}"
2426
+ )
2427
+
2428
+ if len(res) == 1:
2429
+ return res[0]["id"]
2430
+ if fuzzy:
2431
+ return res[0]["id"]
2432
+ raise AmbiguousAccountError(
2433
+ f"Search '{search_param}' matched {len(res)} accounts with no exact "
2434
+ f"id/accountNumber match. Pass an exact identifier, use "
2435
+ f"search_accounts_number_and_name(), or pass fuzzy=True to accept "
2436
+ f"the first match. Candidates: {_candidates(res)}"
2437
+ )
2375
2438
 
2376
2439
  def search_accounts(self, search_param):
2377
2440
  """Search for accounts by various criteria.
@@ -2467,7 +2530,7 @@ class EclipseV1(EclipseBase):
2467
2530
  res = self.api_request(f"{self.base_url}/admin/authorization/user")
2468
2531
  return res.json()["userLoginId"]
2469
2532
 
2470
- def get_set_asides(self, account_id, active_only=False):
2533
+ def get_set_asides(self, account_id=None, active_only=False, *, internal_account_id=None):
2471
2534
  """Get set-aside cash settings for a specific account (v1 surface).
2472
2535
 
2473
2536
  Returns the raw v1 records from ``GET /account/accounts/{id}/asidecash``.
@@ -2479,11 +2542,25 @@ class EclipseV1(EclipseBase):
2479
2542
  account_id: Account ID, number, or name (resolved via search).
2480
2543
  active_only: When True, keep only records where ``isActive`` is truthy.
2481
2544
  Defaults to False (includes expired/inactive).
2545
+ internal_account_id: Eclipse internal account id, used verbatim with
2546
+ no search resolution — the deterministic path for callers that
2547
+ already hold the id. Mutually exclusive with ``account_id``.
2482
2548
 
2483
2549
  Returns:
2484
2550
  list: Raw v1 set-aside records for the account.
2485
- """
2486
- internal_id = self.get_internal_account_id(account_id)
2551
+
2552
+ Raises:
2553
+ ValueError: If both or neither of ``account_id`` /
2554
+ ``internal_account_id`` are given.
2555
+ """
2556
+ if account_id is not None and internal_account_id is not None:
2557
+ raise ValueError("pass account_id (search) or internal_account_id, not both")
2558
+ if internal_account_id is not None:
2559
+ internal_id = int(internal_account_id)
2560
+ elif account_id is not None:
2561
+ internal_id = self.get_internal_account_id(account_id)
2562
+ else:
2563
+ raise ValueError("account_id or internal_account_id is required")
2487
2564
  res = self.api_request(f"{self.base_url}/account/accounts/{internal_id}/asidecash")
2488
2565
  records = res.json()
2489
2566
  if active_only:
@@ -2492,8 +2569,8 @@ class EclipseV1(EclipseBase):
2492
2569
 
2493
2570
  def create_set_aside(
2494
2571
  self,
2495
- account_number,
2496
- amount,
2572
+ account_number=None,
2573
+ amount=None,
2497
2574
  min_amount=0.0,
2498
2575
  max_amount=0.0,
2499
2576
  description=None,
@@ -2506,11 +2583,14 @@ class EclipseV1(EclipseBase):
2506
2583
  deplete_over_time=False,
2507
2584
  percent_calc_type=0,
2508
2585
  sync=True,
2586
+ *,
2587
+ internal_account_id=None,
2509
2588
  ):
2510
2589
  """Create a set-aside cash reservation for an account.
2511
2590
 
2512
2591
  Args:
2513
- account_number: Full custodial account number
2592
+ account_number: Full custodial account number (resolved via search).
2593
+ Mutually exclusive with ``internal_account_id``.
2514
2594
  amount: Cash amount to set aside
2515
2595
  min_amount: Minimum cash amount (default 0.0)
2516
2596
  max_amount: Maximum cash amount (default 0.0)
@@ -2527,11 +2607,27 @@ class EclipseV1(EclipseBase):
2527
2607
  percent_calc_type: 0='Use Default/Managed Value', 1='Use Total Value',
2528
2608
  2='Use Excluded Value' (default 0)
2529
2609
  sync: Wait for analytics to complete (default True)
2610
+ internal_account_id: Eclipse internal account id, used verbatim with
2611
+ no search resolution — the deterministic path for callers that
2612
+ already hold the id. Mutually exclusive with ``account_number``.
2530
2613
 
2531
2614
  Returns:
2532
2615
  dict: Created set-aside details
2533
- """
2534
- account_id = self.get_internal_account_id(account_number)
2616
+
2617
+ Raises:
2618
+ ValueError: If both or neither of ``account_number`` /
2619
+ ``internal_account_id`` are given, or ``amount`` is missing.
2620
+ """
2621
+ if account_number is not None and internal_account_id is not None:
2622
+ raise ValueError("pass account_number (search) or internal_account_id, not both")
2623
+ if amount is None:
2624
+ raise ValueError("amount is required")
2625
+ if internal_account_id is not None:
2626
+ account_id = int(internal_account_id)
2627
+ elif account_number is not None:
2628
+ account_id = self.get_internal_account_id(account_number)
2629
+ else:
2630
+ raise ValueError("account_number or internal_account_id is required")
2535
2631
 
2536
2632
  cash_type_map = {
2537
2633
  # end point account/accounts/asideCashAmountType
@@ -3033,6 +3129,13 @@ class EclipseV1(EclipseBase):
3033
3129
  :meth:`EclipseV2.create_trades` handles buys *and* sells and honours
3034
3130
  share count and dollar amount exactly.
3035
3131
 
3132
+ Orion support's answer to the sell-500 case (2026-08-03) was to use
3133
+ the **v2** wrapper body (``application`` / ``trades[]`` / ``action`` /
3134
+ ``tradeShares``). That body is not accepted here -- posting it to this
3135
+ v1 endpoint returns ``422 "Invalid trade order type id"`` for buys and
3136
+ sells alike -- so it is a redirect to the v2 surface, not a fix for
3137
+ this one. The v1 sell 500 remains unexplained upstream.
3138
+
3036
3139
  The generic per-trade creator behind Eclipse's Quick Trade / journal flows,
3037
3140
  as opposed to the ``/tradetool/`` generators (:meth:`cash_needs_trade`,
3038
3141
  :meth:`rebalance_trade`, ...) which operate on whole portfolios.
@@ -6625,7 +6728,7 @@ class EclipseV2(EclipseBase):
6625
6728
  "end_date": end_date,
6626
6729
  }
6627
6730
 
6628
- def get_set_asides(self, account_id=None, active_only=False):
6731
+ def get_set_asides(self, account_id=None, active_only=False, *, internal_account_id=None):
6629
6732
  """Get set-aside cash reservations, including the Eclipse set-aside id.
6630
6733
 
6631
6734
  Uses the v2 batch endpoint, which accepts a list of internal account IDs
@@ -6637,15 +6740,27 @@ class EclipseV2(EclipseBase):
6637
6740
 
6638
6741
  Args:
6639
6742
  account_id: Account ID, number, or name to restrict to a single
6640
- account. When None (default), returns set-asides firm-wide by
6641
- issuing one batch POST over all accounts.
6743
+ account (resolved via search). When neither this nor
6744
+ ``internal_account_id`` is given, returns set-asides firm-wide
6745
+ by issuing one batch POST over all accounts.
6642
6746
  active_only: When True, return only currently-active set-asides.
6643
6747
  Defaults to False (includes expired/inactive).
6748
+ internal_account_id: Eclipse internal account id, used verbatim with
6749
+ no search resolution — the deterministic path for callers that
6750
+ already hold the id. Mutually exclusive with ``account_id``.
6644
6751
 
6645
6752
  Returns:
6646
6753
  list: Augmented set-aside records
6647
- """
6648
- if account_id is not None:
6754
+
6755
+ Raises:
6756
+ ValueError: If both ``account_id`` and ``internal_account_id`` are
6757
+ given.
6758
+ """
6759
+ if account_id is not None and internal_account_id is not None:
6760
+ raise ValueError("pass account_id (search) or internal_account_id, not both")
6761
+ if internal_account_id is not None:
6762
+ account_ids = [int(internal_account_id)]
6763
+ elif account_id is not None:
6649
6764
  account_ids = [self.get_internal_account_id(account_id)]
6650
6765
  else:
6651
6766
  account_ids = [a["id"] for a in self.get_all_accounts()]
@@ -7328,7 +7443,7 @@ class EclipseV2(EclipseBase):
7328
7443
  def validate_trades(
7329
7444
  self,
7330
7445
  trades,
7331
- application=TRADING_APPLICATION_MANUAL,
7446
+ application=TRADING_APPLICATION_QUICK_TRADES,
7332
7447
  trade_tool_selection=2,
7333
7448
  trade_instance_type=5,
7334
7449
  trade_instance_sub_type=11,
@@ -7355,10 +7470,13 @@ class EclipseV2(EclipseBase):
7355
7470
  Size each with ``shares`` (share count) or ``amount`` (dollars) --
7356
7471
  see :meth:`build_trade` for the full parameter table.
7357
7472
  application: ``TradingApplication`` -- see :data:`TRADING_APPLICATIONS`.
7358
- Defaults to 11 (ManualTrade), the only value that creates orders on
7359
- :meth:`create_trades`. This endpoint tolerates other values, but
7360
- keeping the two in step avoids validating under one application and
7361
- creating under another.
7473
+ Defaults to 11 (``QuickTrades``), the only value that creates orders
7474
+ on :meth:`create_trades`. The enum name is also accepted as a string
7475
+ (``"QuickTrades"``) and behaves identically. This endpoint tolerates
7476
+ other values, but they are not equivalent: ``application=1``
7477
+ (CashNeeds) returns ``isValid: False`` with ``tradeAmount: 0`` rather
7478
+ than pricing the trade, so validating under one application and
7479
+ creating under another gives a meaningless pre-flight.
7362
7480
  trade_tool_selection: 1=Portfolio, 2=Account, 3=Model, ... (default 2)
7363
7481
  trade_instance_type: See :data:`TRADE_INSTANCE_TYPES` (default 5, Quick Trade)
7364
7482
  trade_instance_sub_type: See :data:`TRADE_INSTANCE_SUBTYPES`
@@ -7389,7 +7507,7 @@ class EclipseV2(EclipseBase):
7389
7507
  def create_trades(
7390
7508
  self,
7391
7509
  trades,
7392
- application=TRADING_APPLICATION_MANUAL,
7510
+ application=TRADING_APPLICATION_QUICK_TRADES,
7393
7511
  trade_tool_selection=2,
7394
7512
  trade_instance_type=5,
7395
7513
  trade_instance_sub_type=11,
@@ -7427,7 +7545,8 @@ class EclipseV2(EclipseBase):
7427
7545
  Size each with ``shares`` (share count) or ``amount`` (dollars);
7428
7546
  see :meth:`build_trade` for the full parameter table.
7429
7547
  application: ``TradingApplication`` -- see :data:`TRADING_APPLICATIONS`.
7430
- **Only 11 (ManualTrade) creates orders**, and it is the default.
7548
+ **Only 11 (``QuickTrades``) creates orders**, and it is the default.
7549
+ The enum name is also accepted as a string (``"QuickTrades"``).
7431
7550
  Every other value except 14 returns 400 "Trade Application <Name>
7432
7551
  is not supported."; 14 returns 200 with an empty ``tradeId`` list
7433
7552
  and creates nothing, so it is rejected here rather than allowed to
@@ -7455,11 +7574,11 @@ class EclipseV2(EclipseBase):
7455
7574
  >>> e.create_trades([t])["tradeId"]
7456
7575
  [72495]
7457
7576
  """
7458
- if application != TRADING_APPLICATION_MANUAL:
7577
+ if application != TRADING_APPLICATION_QUICK_TRADES:
7459
7578
  name = TRADING_APPLICATIONS.get(application, "unknown")
7460
7579
  raise ValueError(
7461
7580
  f"application={application} ({name}) does not create trade orders on "
7462
- f"this endpoint; only {TRADING_APPLICATION_MANUAL} (ManualTrade) does. "
7581
+ f"this endpoint; only {TRADING_APPLICATION_QUICK_TRADES} (QuickTrades) does. "
7463
7582
  "Other values return 400, and 14 returns 200 while creating nothing."
7464
7583
  )
7465
7584
  for i, t in enumerate(trades or []):
@@ -11548,13 +11667,19 @@ class Eclipse(EclipseBase):
11548
11667
 
11549
11668
  # --- v2-preferred overrides (documented best-of-both choices) ---------------
11550
11669
 
11551
- def get_set_asides(self, account_id=None, active_only=False):
11670
+ def get_set_asides(self, account_id=None, active_only=False, *, internal_account_id=None):
11552
11671
  """Set-asides via the v2 batch endpoint (best: firm-wide + set-aside id).
11553
11672
 
11554
11673
  Delegates to :meth:`EclipseV2.get_set_asides`. Use ``self.v1.get_set_asides``
11555
- for the raw per-account v1 form.
11674
+ for the raw per-account v1 form. Pass ``internal_account_id=`` when you
11675
+ already hold the Eclipse internal id — it is used verbatim, with no
11676
+ search resolution.
11556
11677
  """
11557
- return self.v2.get_set_asides(account_id=account_id, active_only=active_only)
11678
+ return self.v2.get_set_asides(
11679
+ account_id=account_id,
11680
+ active_only=active_only,
11681
+ internal_account_id=internal_account_id,
11682
+ )
11558
11683
 
11559
11684
 
11560
11685
  class EclipseAPI(Eclipse):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orionapi"
3
- version = "2.27.0"
3
+ version = "2.28.0"
4
4
  description = "A python interface for the Orion Advisors platform web API"
5
5
  authors = ["spencerogden-dsam <67068943+spencerogden-dsam@users.noreply.github.com>"]
6
6
 
File without changes