orionapi 2.29.0__tar.gz → 2.30.2__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,4 +1,4 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.30.2"
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
@@ -2810,6 +2810,11 @@ class EclipseV1(EclipseBase):
|
|
|
2810
2810
|
"""Get portfolio details by ID.
|
|
2811
2811
|
|
|
2812
2812
|
Returns dict with 'general', 'teams', 'issues', 'summary' sections.
|
|
2813
|
+
The 'general' section carries ``teamIds`` / ``primaryTeamId``. The
|
|
2814
|
+
'teams' section's per-record field spelling is unspecified upstream
|
|
2815
|
+
(``id`` vs ``teamId``, ``name`` vs ``teamName``) — accept both, or use
|
|
2816
|
+
:meth:`get_portfolio_teams` for a normalized ``{id, name, is_primary}``
|
|
2817
|
+
view.
|
|
2813
2818
|
"""
|
|
2814
2819
|
res = self.api_request(f"{self.base_url}/portfolio/portfolios/{portfolio_id}")
|
|
2815
2820
|
return res.json()
|
|
@@ -4838,46 +4843,46 @@ class EclipseV1(EclipseBase):
|
|
|
4838
4843
|
def set_portfolio_tradeable(self, portfolio_id, tradeable=True, sync=True):
|
|
4839
4844
|
"""Set whether trading is allowed for a portfolio.
|
|
4840
4845
|
|
|
4846
|
+
Uses the v2 ``setPortfolioTradeBlock`` action, which accepts only
|
|
4847
|
+
``{"id", "doNotTrade"}`` and therefore cannot touch any other portfolio
|
|
4848
|
+
field. The older approach (GET the portfolio, rebuild the v1 PUT payload
|
|
4849
|
+
by hand) rejected portfolios with no primary team and risked stripping
|
|
4850
|
+
teams, trading instructions and auto-rebalance settings that the
|
|
4851
|
+
rebuilt payload did not carry. Verified live 2026-09-10 on portfolio
|
|
4852
|
+
209: the flag flips and every ``general``/``teams`` field is unchanged.
|
|
4853
|
+
|
|
4841
4854
|
Args:
|
|
4842
4855
|
portfolio_id: Portfolio ID
|
|
4843
4856
|
tradeable: True to allow trading, False to block (default True)
|
|
4844
4857
|
sync: Wait for analytics to complete (default True)
|
|
4845
4858
|
|
|
4846
4859
|
Returns:
|
|
4847
|
-
dict: Updated portfolio details
|
|
4860
|
+
dict: Updated portfolio details (same shape as :meth:`get_portfolio`)
|
|
4848
4861
|
"""
|
|
4849
4862
|
if not isinstance(portfolio_id, int) or portfolio_id < 1:
|
|
4850
4863
|
raise ValueError("portfolio_id must be a positive integer")
|
|
4851
4864
|
if not isinstance(tradeable, bool):
|
|
4852
4865
|
raise ValueError("tradeable must be a boolean")
|
|
4853
4866
|
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
general = portfolio.get("general", {})
|
|
4857
|
-
|
|
4858
|
-
# Build payload preserving existing fields
|
|
4859
|
-
payload = {
|
|
4860
|
-
"name": general.get("portfolioName"),
|
|
4861
|
-
"modelId": general.get("modelId"),
|
|
4862
|
-
"isSleevePortfolio": general.get("sleevePortfolio", False),
|
|
4863
|
-
"doNotTrade": 0 if tradeable else 1,
|
|
4864
|
-
"tags": general.get("tags", ""),
|
|
4865
|
-
"teamIds": general.get("teamIds", []),
|
|
4866
|
-
"primaryTeamId": general.get("primaryTeamId"),
|
|
4867
|
-
}
|
|
4868
|
-
|
|
4869
|
-
res = self.api_request(
|
|
4870
|
-
f"{self.base_url}/portfolio/portfolios/{portfolio_id}",
|
|
4867
|
+
self.api_request(
|
|
4868
|
+
f"{self.base_url_v2}/Portfolio/Portfolios/action/setPortfolioTradeBlock",
|
|
4871
4869
|
requests.put,
|
|
4872
|
-
json=
|
|
4870
|
+
json=[{"id": portfolio_id, "doNotTrade": not tradeable}],
|
|
4873
4871
|
)
|
|
4874
|
-
result =
|
|
4872
|
+
result = self.get_portfolio(portfolio_id)
|
|
4875
4873
|
self._maybe_wait_for_analytics(sync)
|
|
4876
4874
|
return result
|
|
4877
4875
|
|
|
4878
4876
|
def set_account_tradeable(self, account_id, trade_restriction="tradeable", sync=True):
|
|
4879
4877
|
"""Set trading restrictions for an account.
|
|
4880
4878
|
|
|
4879
|
+
Uses the v2 ``setAccountTradeBlock`` action, which accepts only
|
|
4880
|
+
``{"id", "isDoNotBuySell", "isCustodialRestriction"}`` and therefore
|
|
4881
|
+
cannot touch any other account field. The older approach rebuilt the
|
|
4882
|
+
v1 ``PUT /account/accounts/{id}`` payload from ``generalSection`` of the
|
|
4883
|
+
GET DTO, but the account DTO is flat (no ``generalSection``), so it was
|
|
4884
|
+
sending ``accountName: null`` / ``portfolioId: null`` alongside the flags.
|
|
4885
|
+
|
|
4881
4886
|
Args:
|
|
4882
4887
|
account_id: Internal Eclipse account ID
|
|
4883
4888
|
trade_restriction: One of:
|
|
@@ -4887,7 +4892,8 @@ class EclipseV1(EclipseBase):
|
|
|
4887
4892
|
sync: Wait for analytics to complete (default True)
|
|
4888
4893
|
|
|
4889
4894
|
Returns:
|
|
4890
|
-
dict: Updated account details
|
|
4895
|
+
dict: Updated account details (same shape as :meth:`get_account_details`;
|
|
4896
|
+
the flags are ``isDoNotBuySell`` and ``isCustodialRestriction``)
|
|
4891
4897
|
|
|
4892
4898
|
Note:
|
|
4893
4899
|
These are mutually exclusive options - only one can be active at a time.
|
|
@@ -4895,35 +4901,27 @@ class EclipseV1(EclipseBase):
|
|
|
4895
4901
|
if not isinstance(account_id, int) or account_id < 1:
|
|
4896
4902
|
raise ValueError("account_id must be a positive integer")
|
|
4897
4903
|
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
# Get current account details
|
|
4903
|
-
account = self.get_account_details(account_id)
|
|
4904
|
-
general = account.get("generalSection", {})
|
|
4905
|
-
|
|
4906
|
-
# Build payload
|
|
4907
|
-
payload = {
|
|
4908
|
-
"accountName": general.get("accountName"),
|
|
4909
|
-
"portfolioId": general.get("portfolioId"),
|
|
4904
|
+
flags = {
|
|
4905
|
+
"tradeable": (False, False),
|
|
4906
|
+
"block_advisor": (True, False),
|
|
4907
|
+
"block_custodian": (False, True),
|
|
4910
4908
|
}
|
|
4909
|
+
if trade_restriction not in flags:
|
|
4910
|
+
raise ValueError(f"trade_restriction must be one of {list(flags)}")
|
|
4911
|
+
do_not_buy_sell, custodial_restriction = flags[trade_restriction]
|
|
4911
4912
|
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
res = self.api_request(
|
|
4924
|
-
f"{self.base_url}/account/accounts/{account_id}", requests.put, json=payload
|
|
4913
|
+
self.api_request(
|
|
4914
|
+
f"{self.base_url_v2}/Account/Accounts/action/setAccountTradeBlock",
|
|
4915
|
+
requests.put,
|
|
4916
|
+
json=[
|
|
4917
|
+
{
|
|
4918
|
+
"id": account_id,
|
|
4919
|
+
"isDoNotBuySell": do_not_buy_sell,
|
|
4920
|
+
"isCustodialRestriction": custodial_restriction,
|
|
4921
|
+
}
|
|
4922
|
+
],
|
|
4925
4923
|
)
|
|
4926
|
-
result =
|
|
4924
|
+
result = self.get_account_details(account_id)
|
|
4927
4925
|
self._maybe_wait_for_analytics(sync)
|
|
4928
4926
|
return result
|
|
4929
4927
|
|
|
@@ -6663,6 +6661,108 @@ class EclipseV1(EclipseBase):
|
|
|
6663
6661
|
json=payload,
|
|
6664
6662
|
).json()
|
|
6665
6663
|
|
|
6664
|
+
# --- Admin: Teams (v1 reads) ---
|
|
6665
|
+
# The v1 team routes are the reliable team read on tenants where teams were
|
|
6666
|
+
# not created with an externalId — EclipseV2.get_teams returns [] there
|
|
6667
|
+
# (see its docstring). teamIds/primaryTeamId are required on the portfolio
|
|
6668
|
+
# record, so these are the reads that unblock create_portfolio.
|
|
6669
|
+
|
|
6670
|
+
def get_teams(self, is_active=None):
|
|
6671
|
+
"""Get all teams (``GET /admin/teams``).
|
|
6672
|
+
|
|
6673
|
+
Args:
|
|
6674
|
+
is_active: When set, passed as ``isActive`` (e.g. ``True`` returns
|
|
6675
|
+
active teams only). When None (default), the parameter is
|
|
6676
|
+
omitted and all teams are returned.
|
|
6677
|
+
|
|
6678
|
+
Returns:
|
|
6679
|
+
list: Team dicts. At minimum ``{id, name}``; the full record also
|
|
6680
|
+
carries ``portfolioAccess``, ``modelAccess``, ``status``,
|
|
6681
|
+
``numberOfUsers``, ``numberOfModels``, ``numberOfPortfolios``,
|
|
6682
|
+
``numberOfAdvisors``, ``isDeleted``, ``createdOn/By``,
|
|
6683
|
+
``editedOn/By``.
|
|
6684
|
+
"""
|
|
6685
|
+
params = {}
|
|
6686
|
+
if is_active is not None:
|
|
6687
|
+
params["isActive"] = str(bool(is_active)).lower()
|
|
6688
|
+
res = self.api_request(f"{self.base_url}/admin/teams", params=params)
|
|
6689
|
+
return res.json()
|
|
6690
|
+
|
|
6691
|
+
def get_team(self, team_id):
|
|
6692
|
+
"""Get details for a single team (``GET /admin/teams/{id}``).
|
|
6693
|
+
|
|
6694
|
+
Args:
|
|
6695
|
+
team_id: Eclipse team ID
|
|
6696
|
+
|
|
6697
|
+
Returns:
|
|
6698
|
+
dict: Team detail record
|
|
6699
|
+
"""
|
|
6700
|
+
res = self.api_request(f"{self.base_url}/admin/teams/{team_id}")
|
|
6701
|
+
return res.json()
|
|
6702
|
+
|
|
6703
|
+
def get_team_portfolios(self, team_id):
|
|
6704
|
+
"""Get portfolios associated with a team (``GET /admin/teams/{id}/portfolios``).
|
|
6705
|
+
|
|
6706
|
+
Args:
|
|
6707
|
+
team_id: Eclipse team ID
|
|
6708
|
+
|
|
6709
|
+
Returns:
|
|
6710
|
+
list: Portfolio dicts (``{id, name, isDeleted, createdOn/By, editedOn/By}``)
|
|
6711
|
+
"""
|
|
6712
|
+
res = self.api_request(f"{self.base_url}/admin/teams/{team_id}/portfolios")
|
|
6713
|
+
return res.json()
|
|
6714
|
+
|
|
6715
|
+
def get_team_primary_portfolios(self, team_id):
|
|
6716
|
+
"""Get portfolios where the team is set as primary
|
|
6717
|
+
(``GET /admin/teams/{id}/primaryPortfolios``).
|
|
6718
|
+
|
|
6719
|
+
Args:
|
|
6720
|
+
team_id: Eclipse team ID
|
|
6721
|
+
|
|
6722
|
+
Returns:
|
|
6723
|
+
list: Portfolio dicts (``{id, name, createdOn/By, editedOn/By}``)
|
|
6724
|
+
"""
|
|
6725
|
+
res = self.api_request(f"{self.base_url}/admin/teams/{team_id}/primaryPortfolios")
|
|
6726
|
+
return res.json()
|
|
6727
|
+
|
|
6728
|
+
def get_portfolio_teams(self, portfolio_id):
|
|
6729
|
+
"""Get the teams stamped on a portfolio, normalized.
|
|
6730
|
+
|
|
6731
|
+
Reads the ``teams`` section of ``GET /portfolio/portfolios/{id}``
|
|
6732
|
+
(see :meth:`get_portfolio`) and normalizes each record to
|
|
6733
|
+
``{id, name, is_primary}``. Unlike the ``/admin/teams`` routes, this
|
|
6734
|
+
works for a credential that is on no team at all, so it is the team
|
|
6735
|
+
read of last resort for service accounts.
|
|
6736
|
+
|
|
6737
|
+
The upstream field spelling is unspecified (``id`` vs ``teamId``,
|
|
6738
|
+
``name`` vs ``teamName``), so both are accepted. ``is_primary`` is
|
|
6739
|
+
derived from ``general.primaryTeamId`` when present, falling back to
|
|
6740
|
+
the record's own ``isPrimaryTeam`` flag.
|
|
6741
|
+
|
|
6742
|
+
Args:
|
|
6743
|
+
portfolio_id: Portfolio ID
|
|
6744
|
+
|
|
6745
|
+
Returns:
|
|
6746
|
+
list: ``{id, name, is_primary}`` dicts, one per team on the portfolio
|
|
6747
|
+
"""
|
|
6748
|
+
portfolio = self.get_portfolio(portfolio_id)
|
|
6749
|
+
primary_team_id = portfolio.get("general", {}).get("primaryTeamId")
|
|
6750
|
+
normalized = []
|
|
6751
|
+
for team in portfolio.get("teams") or []:
|
|
6752
|
+
team_id = team.get("id", team.get("teamId"))
|
|
6753
|
+
if primary_team_id is not None:
|
|
6754
|
+
is_primary = team_id == primary_team_id
|
|
6755
|
+
else:
|
|
6756
|
+
is_primary = bool(team.get("isPrimaryTeam"))
|
|
6757
|
+
normalized.append(
|
|
6758
|
+
{
|
|
6759
|
+
"id": team_id,
|
|
6760
|
+
"name": team.get("name", team.get("teamName")),
|
|
6761
|
+
"is_primary": is_primary,
|
|
6762
|
+
}
|
|
6763
|
+
)
|
|
6764
|
+
return normalized
|
|
6765
|
+
|
|
6666
6766
|
# --- Admin token (v1) ---
|
|
6667
6767
|
|
|
6668
6768
|
def get_firm_token(self, payload):
|
|
@@ -10107,13 +10207,19 @@ class EclipseV2(EclipseBase):
|
|
|
10107
10207
|
# --- Team / ServiceTeams / User ---
|
|
10108
10208
|
|
|
10109
10209
|
def get_teams(self, external_id=None):
|
|
10110
|
-
"""Get teams.
|
|
10210
|
+
"""Get teams (v2 ``Team/Team/GetTeams``).
|
|
10211
|
+
|
|
10212
|
+
Warning:
|
|
10213
|
+
Returns ``[]`` (HTTP 200) on tenants where teams were not created
|
|
10214
|
+
with an ``externalId`` — live-verified 2026-08-17. Prefer the v1
|
|
10215
|
+
route (:meth:`EclipseV1.get_teams`, ``GET /admin/teams``), which
|
|
10216
|
+
the :class:`Eclipse` unifier resolves to.
|
|
10111
10217
|
|
|
10112
10218
|
Args:
|
|
10113
10219
|
external_id: Optional external ID filter (maps to ``externalId``)
|
|
10114
10220
|
|
|
10115
10221
|
Returns:
|
|
10116
|
-
list: Team dicts
|
|
10222
|
+
list: Team dicts (``{teamId, teamName, isPrimaryTeam, ...}``)
|
|
10117
10223
|
"""
|
|
10118
10224
|
params = {}
|
|
10119
10225
|
if external_id is not None:
|
|
@@ -11713,6 +11819,21 @@ class Eclipse(EclipseBase):
|
|
|
11713
11819
|
internal_account_id=internal_account_id,
|
|
11714
11820
|
)
|
|
11715
11821
|
|
|
11822
|
+
# --- v1-preferred overrides (documented best-of-both choices) ----------------
|
|
11823
|
+
|
|
11824
|
+
def get_teams(self, is_active=None):
|
|
11825
|
+
"""Teams via the v1 route (best: v2 ``GetTeams`` is empty on some tenants).
|
|
11826
|
+
|
|
11827
|
+
Delegates to :meth:`EclipseV1.get_teams` (``GET /admin/teams``),
|
|
11828
|
+
returning ``{id, name, ...}`` records. **Behavior change in 2.30.0:**
|
|
11829
|
+
this name previously resolved to :meth:`EclipseV2.get_teams`, which
|
|
11830
|
+
returns ``[]`` (with differently spelled ``{teamId, teamName,
|
|
11831
|
+
isPrimaryTeam}`` records) on tenants where teams were not created with
|
|
11832
|
+
an ``externalId``. Use ``self.v2.get_teams(external_id=...)`` for the
|
|
11833
|
+
v2 surface explicitly.
|
|
11834
|
+
"""
|
|
11835
|
+
return self.v1.get_teams(is_active=is_active)
|
|
11836
|
+
|
|
11716
11837
|
|
|
11717
11838
|
class EclipseAPI(Eclipse):
|
|
11718
11839
|
"""Deprecated alias of :class:`Eclipse` (best-of-both client).
|
|
File without changes
|