orionapi 2.28.0__tar.gz → 2.30.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.28.0
3
+ Version: 2.30.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.28.0"
1
+ __version__ = "2.30.0"
2
2
 
3
3
  import logging
4
4
  import re
@@ -332,6 +332,25 @@ class BaseAPI:
332
332
 
333
333
  return data
334
334
 
335
+ def _request_kwargs(self, timeout=None):
336
+ """Timeout + SSL kwargs for unauthenticated (pre-token) requests.
337
+
338
+ ``login()`` cannot go through :meth:`api_request` — there is no token yet,
339
+ so header construction would raise — but it still needs the same timeout
340
+ and SSL defaults applied here.
341
+
342
+ Args:
343
+ timeout: Per-call timeout in seconds. When None, falls back to the
344
+ client's ``self.timeout``.
345
+
346
+ Returns:
347
+ dict: ``timeout`` and ``verify`` kwargs for ``requests``.
348
+ """
349
+ return {
350
+ "timeout": self.timeout if timeout is None else timeout,
351
+ "verify": self.ca_bundle if self.ca_bundle else self.verify_ssl,
352
+ }
353
+
335
354
  def api_request(self, url, req_func=None, timeout=None, **kwargs):
336
355
  """Make an authenticated API request with error handling.
337
356
 
@@ -440,18 +459,24 @@ class OrionAPI(BaseAPI):
440
459
  self.login(usr, pwd)
441
460
  # Credentials are not stored to prevent memory exposure
442
461
 
443
- def login(self, usr=None, pwd=None):
462
+ def login(self, usr=None, pwd=None, timeout=None):
444
463
  """Authenticate with the Orion API.
445
464
 
446
465
  Args:
447
466
  usr: Username for authentication
448
467
  pwd: Password for authentication
468
+ timeout: Request timeout in seconds. When None (default), falls back
469
+ to the client's ``self.timeout`` (constructor default 30).
449
470
 
450
471
  Raises:
451
472
  AuthenticationError: If credentials are invalid
452
473
  """
453
474
  with self._token_lock:
454
- res = requests.get(f"{self.base_url}/security/token", auth=(usr, pwd))
475
+ res = requests.get(
476
+ f"{self.base_url}/security/token",
477
+ auth=(usr, pwd),
478
+ **self._request_kwargs(timeout),
479
+ )
455
480
  if not res.ok:
456
481
  raise AuthenticationError(f"Login failed: {res.status_code} {res.reason}")
457
482
  try:
@@ -2275,13 +2300,15 @@ class EclipseBase(BaseAPI):
2275
2300
  elif orion_token is not None:
2276
2301
  self.login(orion_token=orion_token)
2277
2302
 
2278
- def login(self, usr=None, pwd=None, orion_token=None):
2303
+ def login(self, usr=None, pwd=None, orion_token=None, timeout=None):
2279
2304
  """Authenticate with the Eclipse API.
2280
2305
 
2281
2306
  Args:
2282
2307
  usr: Username for authentication
2283
2308
  pwd: Password for authentication
2284
2309
  orion_token: Orion session token (alternative to usr/pwd)
2310
+ timeout: Request timeout in seconds. When None (default), falls back
2311
+ to the client's ``self.timeout`` (constructor default 30).
2285
2312
 
2286
2313
  Raises:
2287
2314
  AuthenticationError: If credentials are invalid or missing
@@ -2291,7 +2318,11 @@ class EclipseBase(BaseAPI):
2291
2318
 
2292
2319
  with self._token_lock:
2293
2320
  if usr is not None:
2294
- res = requests.get(f"{self.base_url}/admin/token", auth=(usr, pwd))
2321
+ res = requests.get(
2322
+ f"{self.base_url}/admin/token",
2323
+ auth=(usr, pwd),
2324
+ **self._request_kwargs(timeout),
2325
+ )
2295
2326
  if not res.ok:
2296
2327
  raise AuthenticationError(f"Login failed: {res.status_code} {res.reason}")
2297
2328
  try:
@@ -2303,6 +2334,7 @@ class EclipseBase(BaseAPI):
2303
2334
  res = requests.get(
2304
2335
  f"{self.base_url}/admin/token",
2305
2336
  headers={"Authorization": "Session " + orion_token},
2337
+ **self._request_kwargs(timeout),
2306
2338
  )
2307
2339
  if not res.ok:
2308
2340
  raise AuthenticationError(
@@ -2778,6 +2810,11 @@ class EclipseV1(EclipseBase):
2778
2810
  """Get portfolio details by ID.
2779
2811
 
2780
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.
2781
2818
  """
2782
2819
  res = self.api_request(f"{self.base_url}/portfolio/portfolios/{portfolio_id}")
2783
2820
  return res.json()
@@ -6631,6 +6668,108 @@ class EclipseV1(EclipseBase):
6631
6668
  json=payload,
6632
6669
  ).json()
6633
6670
 
6671
+ # --- Admin: Teams (v1 reads) ---
6672
+ # The v1 team routes are the reliable team read on tenants where teams were
6673
+ # not created with an externalId — EclipseV2.get_teams returns [] there
6674
+ # (see its docstring). teamIds/primaryTeamId are required on the portfolio
6675
+ # record, so these are the reads that unblock create_portfolio.
6676
+
6677
+ def get_teams(self, is_active=None):
6678
+ """Get all teams (``GET /admin/teams``).
6679
+
6680
+ Args:
6681
+ is_active: When set, passed as ``isActive`` (e.g. ``True`` returns
6682
+ active teams only). When None (default), the parameter is
6683
+ omitted and all teams are returned.
6684
+
6685
+ Returns:
6686
+ list: Team dicts. At minimum ``{id, name}``; the full record also
6687
+ carries ``portfolioAccess``, ``modelAccess``, ``status``,
6688
+ ``numberOfUsers``, ``numberOfModels``, ``numberOfPortfolios``,
6689
+ ``numberOfAdvisors``, ``isDeleted``, ``createdOn/By``,
6690
+ ``editedOn/By``.
6691
+ """
6692
+ params = {}
6693
+ if is_active is not None:
6694
+ params["isActive"] = str(bool(is_active)).lower()
6695
+ res = self.api_request(f"{self.base_url}/admin/teams", params=params)
6696
+ return res.json()
6697
+
6698
+ def get_team(self, team_id):
6699
+ """Get details for a single team (``GET /admin/teams/{id}``).
6700
+
6701
+ Args:
6702
+ team_id: Eclipse team ID
6703
+
6704
+ Returns:
6705
+ dict: Team detail record
6706
+ """
6707
+ res = self.api_request(f"{self.base_url}/admin/teams/{team_id}")
6708
+ return res.json()
6709
+
6710
+ def get_team_portfolios(self, team_id):
6711
+ """Get portfolios associated with a team (``GET /admin/teams/{id}/portfolios``).
6712
+
6713
+ Args:
6714
+ team_id: Eclipse team ID
6715
+
6716
+ Returns:
6717
+ list: Portfolio dicts (``{id, name, isDeleted, createdOn/By, editedOn/By}``)
6718
+ """
6719
+ res = self.api_request(f"{self.base_url}/admin/teams/{team_id}/portfolios")
6720
+ return res.json()
6721
+
6722
+ def get_team_primary_portfolios(self, team_id):
6723
+ """Get portfolios where the team is set as primary
6724
+ (``GET /admin/teams/{id}/primaryPortfolios``).
6725
+
6726
+ Args:
6727
+ team_id: Eclipse team ID
6728
+
6729
+ Returns:
6730
+ list: Portfolio dicts (``{id, name, createdOn/By, editedOn/By}``)
6731
+ """
6732
+ res = self.api_request(f"{self.base_url}/admin/teams/{team_id}/primaryPortfolios")
6733
+ return res.json()
6734
+
6735
+ def get_portfolio_teams(self, portfolio_id):
6736
+ """Get the teams stamped on a portfolio, normalized.
6737
+
6738
+ Reads the ``teams`` section of ``GET /portfolio/portfolios/{id}``
6739
+ (see :meth:`get_portfolio`) and normalizes each record to
6740
+ ``{id, name, is_primary}``. Unlike the ``/admin/teams`` routes, this
6741
+ works for a credential that is on no team at all, so it is the team
6742
+ read of last resort for service accounts.
6743
+
6744
+ The upstream field spelling is unspecified (``id`` vs ``teamId``,
6745
+ ``name`` vs ``teamName``), so both are accepted. ``is_primary`` is
6746
+ derived from ``general.primaryTeamId`` when present, falling back to
6747
+ the record's own ``isPrimaryTeam`` flag.
6748
+
6749
+ Args:
6750
+ portfolio_id: Portfolio ID
6751
+
6752
+ Returns:
6753
+ list: ``{id, name, is_primary}`` dicts, one per team on the portfolio
6754
+ """
6755
+ portfolio = self.get_portfolio(portfolio_id)
6756
+ primary_team_id = portfolio.get("general", {}).get("primaryTeamId")
6757
+ normalized = []
6758
+ for team in portfolio.get("teams") or []:
6759
+ team_id = team.get("id", team.get("teamId"))
6760
+ if primary_team_id is not None:
6761
+ is_primary = team_id == primary_team_id
6762
+ else:
6763
+ is_primary = bool(team.get("isPrimaryTeam"))
6764
+ normalized.append(
6765
+ {
6766
+ "id": team_id,
6767
+ "name": team.get("name", team.get("teamName")),
6768
+ "is_primary": is_primary,
6769
+ }
6770
+ )
6771
+ return normalized
6772
+
6634
6773
  # --- Admin token (v1) ---
6635
6774
 
6636
6775
  def get_firm_token(self, payload):
@@ -10075,13 +10214,19 @@ class EclipseV2(EclipseBase):
10075
10214
  # --- Team / ServiceTeams / User ---
10076
10215
 
10077
10216
  def get_teams(self, external_id=None):
10078
- """Get teams.
10217
+ """Get teams (v2 ``Team/Team/GetTeams``).
10218
+
10219
+ Warning:
10220
+ Returns ``[]`` (HTTP 200) on tenants where teams were not created
10221
+ with an ``externalId`` — live-verified 2026-08-17. Prefer the v1
10222
+ route (:meth:`EclipseV1.get_teams`, ``GET /admin/teams``), which
10223
+ the :class:`Eclipse` unifier resolves to.
10079
10224
 
10080
10225
  Args:
10081
10226
  external_id: Optional external ID filter (maps to ``externalId``)
10082
10227
 
10083
10228
  Returns:
10084
- list: Team dicts
10229
+ list: Team dicts (``{teamId, teamName, isPrimaryTeam, ...}``)
10085
10230
  """
10086
10231
  params = {}
10087
10232
  if external_id is not None:
@@ -11681,6 +11826,21 @@ class Eclipse(EclipseBase):
11681
11826
  internal_account_id=internal_account_id,
11682
11827
  )
11683
11828
 
11829
+ # --- v1-preferred overrides (documented best-of-both choices) ----------------
11830
+
11831
+ def get_teams(self, is_active=None):
11832
+ """Teams via the v1 route (best: v2 ``GetTeams`` is empty on some tenants).
11833
+
11834
+ Delegates to :meth:`EclipseV1.get_teams` (``GET /admin/teams``),
11835
+ returning ``{id, name, ...}`` records. **Behavior change in 2.30.0:**
11836
+ this name previously resolved to :meth:`EclipseV2.get_teams`, which
11837
+ returns ``[]`` (with differently spelled ``{teamId, teamName,
11838
+ isPrimaryTeam}`` records) on tenants where teams were not created with
11839
+ an ``externalId``. Use ``self.v2.get_teams(external_id=...)`` for the
11840
+ v2 surface explicitly.
11841
+ """
11842
+ return self.v1.get_teams(is_active=is_active)
11843
+
11684
11844
 
11685
11845
  class EclipseAPI(Eclipse):
11686
11846
  """Deprecated alias of :class:`Eclipse` (best-of-both client).
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orionapi"
3
- version = "2.28.0"
3
+ version = "2.30.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