Skip to content

Commit 0e5a800

Browse files
jfrench9claude
andauthored
Add investor API endpoints for portfolio, position, and security management (#75)
## Summary Introduces the core RoboInvestor client module, adding a complete set of investor API endpoints and corresponding data models for managing portfolios, positions, securities, and holdings. This is a foundational feature that enables programmatic interaction with the investor service. ## Key Accomplishments ### New API Endpoints (`robosystems_client/api/investor/`) - **Portfolios**: Full CRUD operations — `create_portfolio`, `get_portfolio`, `list_portfolios`, `update_portfolio`, `delete_portfolio` - **Positions**: Full CRUD operations — `create_position`, `get_position`, `list_positions`, `update_position`, `delete_position` - **Securities**: Full CRUD operations — `create_security`, `get_security`, `list_securities`, `update_security`, `delete_security` - **Holdings**: Read support via `list_holdings` ### New Data Models (`robosystems_client/models/`) - **Request models**: `CreatePortfolioRequest`, `CreatePositionRequest`, `CreateSecurityRequest`, `UpdatePortfolioRequest`, `UpdatePositionRequest`, `UpdateSecurityRequest` - **Response models**: `PortfolioResponse`, `PositionResponse`, `SecurityResponse`, `HoldingResponse`, `HoldingSecuritySummary` - **List response models**: `PortfolioListResponse`, `PositionListResponse`, `SecurityListResponse`, `HoldingsListResponse` - **Supporting models**: `CreateSecurityRequestTerms`, `SecurityResponseTerms`, `UpdateSecurityRequestTermsType0` ### Modified Files - **`robosystems_client/models/__init__.py`**: Updated to export all new model classes - **`robosystems_client/api/subgraphs/create_subgraph.py`**: Minor modifications (likely import or structural alignment) ## Scope - **37 files changed** across API endpoints and models - **~5,840 lines added**, representing a comprehensive, auto-generated (or consistently structured) client SDK layer - List endpoints for positions and securities include filtering/query parameter support (~256 lines each), suggesting rich querying capabilities ## Breaking Changes None expected. This is a purely additive feature introducing a new `investor` API module alongside existing modules (e.g., `subgraphs`). The minor modification to `create_subgraph.py` should be reviewed to confirm backward compatibility. ## Testing Notes - Verify each endpoint (create, get, list, update, delete) for portfolios, positions, and securities against a running investor API service - Validate request/response serialization and deserialization for all new models - Confirm list endpoints correctly handle pagination and filtering parameters - Test error handling and edge cases (e.g., missing required fields, invalid IDs) - Ensure the `create_subgraph` modification does not regress existing subgraph functionality ## Infrastructure Considerations - The investor API service must be available and configured for client connectivity - Environment or client configuration may need to be updated to include investor service base URLs or authentication credentials - Consider API versioning strategy as this represents a significant new surface area --- 🤖 Generated with [Claude Code](https://claude.ai/code) **Branch Info:** - Source: `feature/roboinvestor-core` - Target: `main` - Type: feature Co-Authored-By: Claude <noreply@anthropic.com>
2 parents e0ac365 + f0097f9 commit 0e5a800

37 files changed

+5840
-13
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Contains endpoint functions for accessing the API"""
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from http import HTTPStatus
2+
from typing import Any
3+
from urllib.parse import quote
4+
5+
import httpx
6+
7+
from ... import errors
8+
from ...client import AuthenticatedClient, Client
9+
from ...models.create_portfolio_request import CreatePortfolioRequest
10+
from ...models.http_validation_error import HTTPValidationError
11+
from ...models.portfolio_response import PortfolioResponse
12+
from ...types import Response
13+
14+
15+
def _get_kwargs(
16+
graph_id: str,
17+
*,
18+
body: CreatePortfolioRequest,
19+
) -> dict[str, Any]:
20+
headers: dict[str, Any] = {}
21+
22+
_kwargs: dict[str, Any] = {
23+
"method": "post",
24+
"url": "/v1/investor/{graph_id}/portfolios".format(
25+
graph_id=quote(str(graph_id), safe=""),
26+
),
27+
}
28+
29+
_kwargs["json"] = body.to_dict()
30+
31+
headers["Content-Type"] = "application/json"
32+
33+
_kwargs["headers"] = headers
34+
return _kwargs
35+
36+
37+
def _parse_response(
38+
*, client: AuthenticatedClient | Client, response: httpx.Response
39+
) -> HTTPValidationError | PortfolioResponse | None:
40+
if response.status_code == 201:
41+
response_201 = PortfolioResponse.from_dict(response.json())
42+
43+
return response_201
44+
45+
if response.status_code == 422:
46+
response_422 = HTTPValidationError.from_dict(response.json())
47+
48+
return response_422
49+
50+
if client.raise_on_unexpected_status:
51+
raise errors.UnexpectedStatus(response.status_code, response.content)
52+
else:
53+
return None
54+
55+
56+
def _build_response(
57+
*, client: AuthenticatedClient | Client, response: httpx.Response
58+
) -> Response[HTTPValidationError | PortfolioResponse]:
59+
return Response(
60+
status_code=HTTPStatus(response.status_code),
61+
content=response.content,
62+
headers=response.headers,
63+
parsed=_parse_response(client=client, response=response),
64+
)
65+
66+
67+
def sync_detailed(
68+
graph_id: str,
69+
*,
70+
client: AuthenticatedClient,
71+
body: CreatePortfolioRequest,
72+
) -> Response[HTTPValidationError | PortfolioResponse]:
73+
"""Create Portfolio
74+
75+
Args:
76+
graph_id (str):
77+
body (CreatePortfolioRequest):
78+
79+
Raises:
80+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
81+
httpx.TimeoutException: If the request takes longer than Client.timeout.
82+
83+
Returns:
84+
Response[HTTPValidationError | PortfolioResponse]
85+
"""
86+
87+
kwargs = _get_kwargs(
88+
graph_id=graph_id,
89+
body=body,
90+
)
91+
92+
response = client.get_httpx_client().request(
93+
**kwargs,
94+
)
95+
96+
return _build_response(client=client, response=response)
97+
98+
99+
def sync(
100+
graph_id: str,
101+
*,
102+
client: AuthenticatedClient,
103+
body: CreatePortfolioRequest,
104+
) -> HTTPValidationError | PortfolioResponse | None:
105+
"""Create Portfolio
106+
107+
Args:
108+
graph_id (str):
109+
body (CreatePortfolioRequest):
110+
111+
Raises:
112+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
113+
httpx.TimeoutException: If the request takes longer than Client.timeout.
114+
115+
Returns:
116+
HTTPValidationError | PortfolioResponse
117+
"""
118+
119+
return sync_detailed(
120+
graph_id=graph_id,
121+
client=client,
122+
body=body,
123+
).parsed
124+
125+
126+
async def asyncio_detailed(
127+
graph_id: str,
128+
*,
129+
client: AuthenticatedClient,
130+
body: CreatePortfolioRequest,
131+
) -> Response[HTTPValidationError | PortfolioResponse]:
132+
"""Create Portfolio
133+
134+
Args:
135+
graph_id (str):
136+
body (CreatePortfolioRequest):
137+
138+
Raises:
139+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
140+
httpx.TimeoutException: If the request takes longer than Client.timeout.
141+
142+
Returns:
143+
Response[HTTPValidationError | PortfolioResponse]
144+
"""
145+
146+
kwargs = _get_kwargs(
147+
graph_id=graph_id,
148+
body=body,
149+
)
150+
151+
response = await client.get_async_httpx_client().request(**kwargs)
152+
153+
return _build_response(client=client, response=response)
154+
155+
156+
async def asyncio(
157+
graph_id: str,
158+
*,
159+
client: AuthenticatedClient,
160+
body: CreatePortfolioRequest,
161+
) -> HTTPValidationError | PortfolioResponse | None:
162+
"""Create Portfolio
163+
164+
Args:
165+
graph_id (str):
166+
body (CreatePortfolioRequest):
167+
168+
Raises:
169+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
170+
httpx.TimeoutException: If the request takes longer than Client.timeout.
171+
172+
Returns:
173+
HTTPValidationError | PortfolioResponse
174+
"""
175+
176+
return (
177+
await asyncio_detailed(
178+
graph_id=graph_id,
179+
client=client,
180+
body=body,
181+
)
182+
).parsed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from http import HTTPStatus
2+
from typing import Any
3+
from urllib.parse import quote
4+
5+
import httpx
6+
7+
from ... import errors
8+
from ...client import AuthenticatedClient, Client
9+
from ...models.create_position_request import CreatePositionRequest
10+
from ...models.http_validation_error import HTTPValidationError
11+
from ...models.position_response import PositionResponse
12+
from ...types import Response
13+
14+
15+
def _get_kwargs(
16+
graph_id: str,
17+
*,
18+
body: CreatePositionRequest,
19+
) -> dict[str, Any]:
20+
headers: dict[str, Any] = {}
21+
22+
_kwargs: dict[str, Any] = {
23+
"method": "post",
24+
"url": "/v1/investor/{graph_id}/positions".format(
25+
graph_id=quote(str(graph_id), safe=""),
26+
),
27+
}
28+
29+
_kwargs["json"] = body.to_dict()
30+
31+
headers["Content-Type"] = "application/json"
32+
33+
_kwargs["headers"] = headers
34+
return _kwargs
35+
36+
37+
def _parse_response(
38+
*, client: AuthenticatedClient | Client, response: httpx.Response
39+
) -> HTTPValidationError | PositionResponse | None:
40+
if response.status_code == 201:
41+
response_201 = PositionResponse.from_dict(response.json())
42+
43+
return response_201
44+
45+
if response.status_code == 422:
46+
response_422 = HTTPValidationError.from_dict(response.json())
47+
48+
return response_422
49+
50+
if client.raise_on_unexpected_status:
51+
raise errors.UnexpectedStatus(response.status_code, response.content)
52+
else:
53+
return None
54+
55+
56+
def _build_response(
57+
*, client: AuthenticatedClient | Client, response: httpx.Response
58+
) -> Response[HTTPValidationError | PositionResponse]:
59+
return Response(
60+
status_code=HTTPStatus(response.status_code),
61+
content=response.content,
62+
headers=response.headers,
63+
parsed=_parse_response(client=client, response=response),
64+
)
65+
66+
67+
def sync_detailed(
68+
graph_id: str,
69+
*,
70+
client: AuthenticatedClient,
71+
body: CreatePositionRequest,
72+
) -> Response[HTTPValidationError | PositionResponse]:
73+
"""Create Position
74+
75+
Args:
76+
graph_id (str):
77+
body (CreatePositionRequest):
78+
79+
Raises:
80+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
81+
httpx.TimeoutException: If the request takes longer than Client.timeout.
82+
83+
Returns:
84+
Response[HTTPValidationError | PositionResponse]
85+
"""
86+
87+
kwargs = _get_kwargs(
88+
graph_id=graph_id,
89+
body=body,
90+
)
91+
92+
response = client.get_httpx_client().request(
93+
**kwargs,
94+
)
95+
96+
return _build_response(client=client, response=response)
97+
98+
99+
def sync(
100+
graph_id: str,
101+
*,
102+
client: AuthenticatedClient,
103+
body: CreatePositionRequest,
104+
) -> HTTPValidationError | PositionResponse | None:
105+
"""Create Position
106+
107+
Args:
108+
graph_id (str):
109+
body (CreatePositionRequest):
110+
111+
Raises:
112+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
113+
httpx.TimeoutException: If the request takes longer than Client.timeout.
114+
115+
Returns:
116+
HTTPValidationError | PositionResponse
117+
"""
118+
119+
return sync_detailed(
120+
graph_id=graph_id,
121+
client=client,
122+
body=body,
123+
).parsed
124+
125+
126+
async def asyncio_detailed(
127+
graph_id: str,
128+
*,
129+
client: AuthenticatedClient,
130+
body: CreatePositionRequest,
131+
) -> Response[HTTPValidationError | PositionResponse]:
132+
"""Create Position
133+
134+
Args:
135+
graph_id (str):
136+
body (CreatePositionRequest):
137+
138+
Raises:
139+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
140+
httpx.TimeoutException: If the request takes longer than Client.timeout.
141+
142+
Returns:
143+
Response[HTTPValidationError | PositionResponse]
144+
"""
145+
146+
kwargs = _get_kwargs(
147+
graph_id=graph_id,
148+
body=body,
149+
)
150+
151+
response = await client.get_async_httpx_client().request(**kwargs)
152+
153+
return _build_response(client=client, response=response)
154+
155+
156+
async def asyncio(
157+
graph_id: str,
158+
*,
159+
client: AuthenticatedClient,
160+
body: CreatePositionRequest,
161+
) -> HTTPValidationError | PositionResponse | None:
162+
"""Create Position
163+
164+
Args:
165+
graph_id (str):
166+
body (CreatePositionRequest):
167+
168+
Raises:
169+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
170+
httpx.TimeoutException: If the request takes longer than Client.timeout.
171+
172+
Returns:
173+
HTTPValidationError | PositionResponse
174+
"""
175+
176+
return (
177+
await asyncio_detailed(
178+
graph_id=graph_id,
179+
client=client,
180+
body=body,
181+
)
182+
).parsed

0 commit comments

Comments
 (0)