From 01f5f32bde44118e0997ee70e9e96a50643a3984 Mon Sep 17 00:00:00 2001 From: Hyperion <89154626+daniel-scrivner@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:35:13 -0600 Subject: [PATCH 1/2] feat: add Special Situations SDK workflows --- README.md | 61 +++++++++- client.go | 298 ++++++++++++++++++++++++++++++++++++++++++++----- client_test.go | 244 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 568 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 5192c12..f31a49f 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,62 @@ history, err := client.Factors.History("VALUE", map[string]string{ ``` Start with `client.Entities`, `client.Filings`, `client.Sections`, -`client.Search`, and `client.Factors` when exploring. Use the flat methods when -you need an endpoint outside those high-signal groups. The service fields are -initialized by `secapi.NewClient`, `secapi.NewBearerTokenClient`, and -`secapi.NewSecApiClient`; if you construct `Client` literals manually, continue -using the flat methods or switch to the constructors before using grouped fields. +`client.Search`, `client.Factors`, and `client.Situations` when exploring. Use +the flat methods when you need an endpoint outside those high-signal groups. The +service fields are initialized by `secapi.NewClient`, +`secapi.NewBearerTokenClient`, and `secapi.NewSecApiClient`; if you construct +`Client` literals manually, continue using the flat methods or switch to the +constructors before using grouped fields. + +## Special Situations + +Special Situations helpers cover the public SEC-derived situations API: list, +detail, filings timeline, summary, Copy-for-LLM export, feed, calendar, stats, +performance, and EDGAR form lookup. + +```go +client := secapi.NewClient(os.Getenv("SECAPI_API_KEY")) + +situations, err := client.Situations.List(map[string]string{ + "types": "tender_offer,bankruptcy", + "tickers": "AAPL,MSFT", + "limit": "10", + "response_mode": "compact", +}) +if err != nil { + panic(err) +} +fmt.Println(situations["data"]) + +detail, err := client.Situations.Get("sit_123", map[string]string{ + "enrich": "true", +}) +if err != nil { + panic(err) +} +fmt.Println(detail["headline"]) + +markdown, err := client.Situations.Export("sit_123", nil) +if err != nil { + panic(err) +} +fmt.Println(markdown) + +calendar, err := client.Situations.Calendar(map[string]string{ + "days": "45", + "date_types": "vote_date,tender_expiration", +}) +if err != nil { + panic(err) +} +fmt.Println(calendar["data"]) +``` + +Archive issue helpers read the immutable paid weekly issue archive: +`client.Situations.Issues(...)` and `client.Situations.Issue(...)`, with flat +aliases `ListSituationIssues` and `GetSituationIssue`. These archive endpoints +depend on unmerged datastream PR #1363, so they require an API deployment that +includes that server change before they will work against production. ## Auto-pagination diff --git a/client.go b/client.go index 260cf34..ddbac9f 100644 --- a/client.go +++ b/client.go @@ -427,6 +427,7 @@ type Client struct { Sections *SectionService Search *SearchService Factors *FactorService + Situations *SituationService } // APIError is returned for non-2xx SEC API responses. @@ -483,6 +484,7 @@ func newClient(apiKey string, bearerToken string) *Client { client.Sections = &SectionService{client: client} client.Search = &SearchService{client: client} client.Factors = &FactorService{client: client} + client.Situations = &SituationService{client: client} return client } @@ -742,6 +744,106 @@ func (s *FactorService) Custom(body any, params ...map[string]string) (map[strin return s.client.FactorCustom(body, params...) } +type SituationService struct { + client *Client +} + +func (s *SituationService) List(params map[string]string) (map[string]any, error) { + return s.client.ListSituations(params) +} + +func (s *SituationService) ListWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return s.client.ListSituationsWithContext(ctx, params) +} + +func (s *SituationService) Issues(params map[string]string) (map[string]any, error) { + return s.client.ListSituationIssues(params) +} + +func (s *SituationService) IssuesWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return s.client.ListSituationIssuesWithContext(ctx, params) +} + +func (s *SituationService) Issue(issue string, params map[string]string) (map[string]any, error) { + return s.client.GetSituationIssue(issue, params) +} + +func (s *SituationService) IssueWithContext(ctx context.Context, issue string, params map[string]string) (map[string]any, error) { + return s.client.GetSituationIssueWithContext(ctx, issue, params) +} + +func (s *SituationService) Get(situationID string, params map[string]string) (map[string]any, error) { + return s.client.GetSituation(situationID, params) +} + +func (s *SituationService) GetWithContext(ctx context.Context, situationID string, params map[string]string) (map[string]any, error) { + return s.client.GetSituationWithContext(ctx, situationID, params) +} + +func (s *SituationService) Filings(situationID string, params map[string]string) (map[string]any, error) { + return s.client.SituationFilings(situationID, params) +} + +func (s *SituationService) FilingsWithContext(ctx context.Context, situationID string, params map[string]string) (map[string]any, error) { + return s.client.SituationFilingsWithContext(ctx, situationID, params) +} + +func (s *SituationService) Summary(situationID string, params map[string]string) (map[string]any, error) { + return s.client.SituationSummary(situationID, params) +} + +func (s *SituationService) SummaryWithContext(ctx context.Context, situationID string, params map[string]string) (map[string]any, error) { + return s.client.SituationSummaryWithContext(ctx, situationID, params) +} + +func (s *SituationService) Export(situationID string, params map[string]string) (string, error) { + return s.client.ExportSituation(situationID, params) +} + +func (s *SituationService) ExportWithContext(ctx context.Context, situationID string, params map[string]string) (string, error) { + return s.client.ExportSituationWithContext(ctx, situationID, params) +} + +func (s *SituationService) Feed(params map[string]string) (map[string]any, error) { + return s.client.SituationFeed(params) +} + +func (s *SituationService) FeedWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return s.client.SituationFeedWithContext(ctx, params) +} + +func (s *SituationService) Calendar(params map[string]string) (map[string]any, error) { + return s.client.SituationCalendar(params) +} + +func (s *SituationService) CalendarWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return s.client.SituationCalendarWithContext(ctx, params) +} + +func (s *SituationService) Stats(params map[string]string) (map[string]any, error) { + return s.client.SituationStats(params) +} + +func (s *SituationService) StatsWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return s.client.SituationStatsWithContext(ctx, params) +} + +func (s *SituationService) Performance(params map[string]string) (map[string]any, error) { + return s.client.SituationPerformance(params) +} + +func (s *SituationService) PerformanceWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return s.client.SituationPerformanceWithContext(ctx, params) +} + +func (s *SituationService) ByForm(form string, params map[string]string) (map[string]any, error) { + return s.client.SituationsByForm(form, params) +} + +func (s *SituationService) ByFormWithContext(ctx context.Context, form string, params map[string]string) (map[string]any, error) { + return s.client.SituationsByFormWithContext(ctx, form, params) +} + func firstEnv(names ...string) string { for _, name := range names { if value := strings.TrimSpace(os.Getenv(name)); value != "" { @@ -1064,7 +1166,65 @@ func (c *Client) request(method string, pathname string, params map[string]strin return c.requestWithContext(context.Background(), method, pathname, params, body) } +func (c *Client) requestTextWithContext(ctx context.Context, method string, pathname string, params map[string]string, body any) (string, error) { + raw, err := c.requestRawWithContext(ctx, method, pathname, params, body) + if err != nil { + return "", err + } + return string(raw), nil +} + func (c *Client) requestWithContext(ctx context.Context, method string, pathname string, params map[string]string, body any) (map[string]any, error) { + responsePayload, res, err := c.requestRawResponseWithContext(ctx, method, pathname, params, body) + if err != nil { + return nil, err + } + if len(responsePayload) == 0 && isSuccessStatus(res.StatusCode) { + return nil, nil + } + if len(responsePayload) == 0 { + return nil, apiError(res, nil) + } + var decoded map[string]any + if err := json.Unmarshal(responsePayload, &decoded); err != nil { + if !isSuccessStatus(res.StatusCode) { + return nil, &APIError{ + StatusCode: res.StatusCode, + RequestID: responseRequestID(res, nil), + Message: strings.TrimSpace(string(responsePayload)), + } + } + return nil, err + } + if !isSuccessStatus(res.StatusCode) { + return nil, apiError(res, decoded) + } + return decoded, nil +} + +func (c *Client) requestRawWithContext(ctx context.Context, method string, pathname string, params map[string]string, body any) ([]byte, error) { + responsePayload, res, err := c.requestRawResponseWithContext(ctx, method, pathname, params, body) + if err != nil { + return nil, err + } + if !isSuccessStatus(res.StatusCode) { + if len(responsePayload) == 0 { + return nil, apiError(res, nil) + } + var decoded map[string]any + if err := json.Unmarshal(responsePayload, &decoded); err == nil { + return nil, apiError(res, decoded) + } + return nil, &APIError{ + StatusCode: res.StatusCode, + RequestID: responseRequestID(res, nil), + Message: strings.TrimSpace(string(responsePayload)), + } + } + return responsePayload, nil +} + +func (c *Client) requestRawResponseWithContext(ctx context.Context, method string, pathname string, params map[string]string, body any) ([]byte, *http.Response, error) { if ctx == nil { ctx = context.Background() } @@ -1074,7 +1234,7 @@ func (c *Client) requestWithContext(ctx context.Context, method string, pathname } u, err := url.Parse(baseURL + pathname) if err != nil { - return nil, err + return nil, nil, err } query := u.Query() for key, value := range params { @@ -1089,7 +1249,7 @@ func (c *Client) requestWithContext(ctx context.Context, method string, pathname if body != nil { payload, err = json.Marshal(body) if err != nil { - return nil, err + return nil, nil, err } } @@ -1102,7 +1262,7 @@ func (c *Client) requestWithContext(ctx context.Context, method string, pathname req, err := http.NewRequestWithContext(ctx, method, u.String(), reader) if err != nil { - return nil, err + return nil, nil, err } req.Header.Set("content-type", "application/json") req.Header.Set("accept", "application/json") @@ -1119,52 +1279,32 @@ func (c *Client) requestWithContext(ctx context.Context, method string, pathname if err != nil { if isRetryableMethod(method) && ctx.Err() == nil && attempt < cfg.MaxRetries { if sleepErr := sleepContext(ctx, retryDelay(attempt, cfg, nil)); sleepErr != nil { - return nil, sleepErr + return nil, nil, sleepErr } continue } - return nil, err + return nil, nil, err } if shouldRetryResponse(method, res.StatusCode) && attempt < cfg.MaxRetries { _, _ = io.Copy(io.Discard, res.Body) _ = res.Body.Close() if err := sleepContext(ctx, retryDelay(attempt, cfg, res)); err != nil { - return nil, err + return nil, nil, err } continue } defer res.Body.Close() if res.StatusCode == http.StatusNoContent { - return nil, nil + return nil, res, nil } responsePayload, err := io.ReadAll(res.Body) if err != nil { - return nil, err - } - if len(responsePayload) == 0 && isSuccessStatus(res.StatusCode) { - return nil, nil + return nil, nil, err } - if len(responsePayload) == 0 { - return nil, apiError(res, nil) - } - var decoded map[string]any - if err := json.Unmarshal(responsePayload, &decoded); err != nil { - if !isSuccessStatus(res.StatusCode) { - return nil, &APIError{ - StatusCode: res.StatusCode, - RequestID: responseRequestID(res, nil), - Message: strings.TrimSpace(string(responsePayload)), - } - } - return nil, err - } - if !isSuccessStatus(res.StatusCode) { - return nil, apiError(res, decoded) - } - return decoded, nil + return responsePayload, res, nil } } @@ -1372,6 +1512,106 @@ func (c *Client) StatementAgentWithContext(ctx context.Context, statementKey str return decodeResponse[AgentStatement](body) } +func (c *Client) ListSituations(params map[string]string) (map[string]any, error) { + return c.ListSituationsWithContext(context.Background(), params) +} + +func (c *Client) ListSituationsWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations", params, nil) +} + +// ListSituationIssues reads the immutable paid weekly issue archive introduced +// by datastream PR #1363. Calls require an API deployment that includes #1363. +func (c *Client) ListSituationIssues(params map[string]string) (map[string]any, error) { + return c.ListSituationIssuesWithContext(context.Background(), params) +} + +func (c *Client) ListSituationIssuesWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/issues", params, nil) +} + +// GetSituationIssue reads an immutable weekly issue by number or slug. This +// route depends on datastream PR #1363 being deployed by the API service. +func (c *Client) GetSituationIssue(issue string, params map[string]string) (map[string]any, error) { + return c.GetSituationIssueWithContext(context.Background(), issue, params) +} + +func (c *Client) GetSituationIssueWithContext(ctx context.Context, issue string, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/issues/"+url.PathEscape(issue), params, nil) +} + +func (c *Client) SituationFeed(params map[string]string) (map[string]any, error) { + return c.SituationFeedWithContext(context.Background(), params) +} + +func (c *Client) SituationFeedWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/feed", params, nil) +} + +func (c *Client) SituationCalendar(params map[string]string) (map[string]any, error) { + return c.SituationCalendarWithContext(context.Background(), params) +} + +func (c *Client) SituationCalendarWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/calendar", params, nil) +} + +func (c *Client) SituationStats(params map[string]string) (map[string]any, error) { + return c.SituationStatsWithContext(context.Background(), params) +} + +func (c *Client) SituationStatsWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/stats", params, nil) +} + +func (c *Client) SituationPerformance(params map[string]string) (map[string]any, error) { + return c.SituationPerformanceWithContext(context.Background(), params) +} + +func (c *Client) SituationPerformanceWithContext(ctx context.Context, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/performance", params, nil) +} + +func (c *Client) SituationsByForm(form string, params map[string]string) (map[string]any, error) { + return c.SituationsByFormWithContext(context.Background(), form, params) +} + +func (c *Client) SituationsByFormWithContext(ctx context.Context, form string, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/by-form/"+url.PathEscape(form), params, nil) +} + +func (c *Client) GetSituation(situationID string, params map[string]string) (map[string]any, error) { + return c.GetSituationWithContext(context.Background(), situationID, params) +} + +func (c *Client) GetSituationWithContext(ctx context.Context, situationID string, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/"+url.PathEscape(situationID), params, nil) +} + +func (c *Client) SituationFilings(situationID string, params map[string]string) (map[string]any, error) { + return c.SituationFilingsWithContext(context.Background(), situationID, params) +} + +func (c *Client) SituationFilingsWithContext(ctx context.Context, situationID string, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/"+url.PathEscape(situationID)+"/filings", params, nil) +} + +func (c *Client) SituationSummary(situationID string, params map[string]string) (map[string]any, error) { + return c.SituationSummaryWithContext(context.Background(), situationID, params) +} + +func (c *Client) SituationSummaryWithContext(ctx context.Context, situationID string, params map[string]string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/"+url.PathEscape(situationID)+"/summary", params, nil) +} + +func (c *Client) ExportSituation(situationID string, params map[string]string) (string, error) { + return c.ExportSituationWithContext(context.Background(), situationID, params) +} + +func (c *Client) ExportSituationWithContext(ctx context.Context, situationID string, params map[string]string) (string, error) { + return c.requestTextWithContext(ctx, http.MethodGet, "/v1/situations/"+url.PathEscape(situationID)+"/export", params, nil) +} + func (c *Client) Offerings(params map[string]string) (map[string]any, error) { return c.request(http.MethodGet, "/v1/offerings", params, nil) } diff --git a/client_test.go b/client_test.go index 12e5ca7..ebf0b61 100644 --- a/client_test.go +++ b/client_test.go @@ -285,6 +285,15 @@ func TestGroupedServiceFieldsDelegateToFlatClientMethods(t *testing.T) { wantPath: "/v1/factors/dashboard", wantQuery: map[string]string{"country": "US", "category": "style", "ticker": "AAPL", "response_mode": "compact"}, }, + { + name: "situations list", + call: func() error { + _, err := client.Situations.List(map[string]string{"types": "tender_offer", "limit": "10", "response_mode": "compact"}) + return err + }, + wantPath: "/v1/situations", + wantQuery: map[string]string{"types": "tender_offer", "limit": "10", "response_mode": "compact"}, + }, } for _, test := range calls { @@ -320,7 +329,7 @@ func TestConstructorsInitializeGroupedServiceFields(t *testing.T) { } for i, client := range clients { - if client.Entities == nil || client.Filings == nil || client.Sections == nil || client.Search == nil || client.Factors == nil { + if client.Entities == nil || client.Filings == nil || client.Sections == nil || client.Search == nil || client.Factors == nil || client.Situations == nil { t.Fatalf("client %d has nil grouped service field", i) } } @@ -992,6 +1001,239 @@ func TestMarketWrappersRouteToMarketPaths(t *testing.T) { } } +func TestSituationWrappersRouteToPublicSituationPaths(t *testing.T) { + client, captured, closeServer := newCaptureClient(t) + defer closeServer() + + calls := []struct { + name string + call func() error + wantPath string + wantQuery map[string]string + }{ + { + name: "list", + call: func() error { + _, err := client.ListSituations(map[string]string{"types": "tender_offer,bankruptcy", "tickers": "AAPL", "limit": "25", "cursor": "50"}) + return err + }, + wantPath: "/v1/situations", + wantQuery: map[string]string{"types": "tender_offer,bankruptcy", "tickers": "AAPL", "limit": "25", "cursor": "50"}, + }, + { + name: "archive issues", + call: func() error { + _, err := client.ListSituationIssues(map[string]string{"limit": "12", "response_mode": "compact"}) + return err + }, + wantPath: "/v1/situations/issues", + wantQuery: map[string]string{"limit": "12", "response_mode": "compact"}, + }, + { + name: "archive issue detail", + call: func() error { + _, err := client.GetSituationIssue("special situations/issue 1", map[string]string{"response_mode": "compact"}) + return err + }, + wantPath: "/v1/situations/issues/special%20situations%2Fissue%201", + wantQuery: map[string]string{"response_mode": "compact"}, + }, + { + name: "feed", + call: func() error { + _, err := client.SituationFeed(map[string]string{"categories": "event", "since": "2026-07-01", "limit": "10"}) + return err + }, + wantPath: "/v1/situations/feed", + wantQuery: map[string]string{"categories": "event", "since": "2026-07-01", "limit": "10"}, + }, + { + name: "calendar", + call: func() error { + _, err := client.SituationCalendar(map[string]string{"date_types": "vote_date", "days": "45"}) + return err + }, + wantPath: "/v1/situations/calendar", + wantQuery: map[string]string{"date_types": "vote_date", "days": "45"}, + }, + { + name: "stats", + call: func() error { + _, err := client.SituationStats(map[string]string{"window": "30d"}) + return err + }, + wantPath: "/v1/situations/stats", + wantQuery: map[string]string{"window": "30d"}, + }, + { + name: "performance", + call: func() error { + _, err := client.SituationPerformance(map[string]string{"types": "tender_offer", "group_by": "subtype"}) + return err + }, + wantPath: "/v1/situations/performance", + wantQuery: map[string]string{"types": "tender_offer", "group_by": "subtype"}, + }, + { + name: "by form", + call: func() error { + _, err := client.SituationsByForm("SC TO-I/A", map[string]string{"statuses": "open", "enrich": "false"}) + return err + }, + wantPath: "/v1/situations/by-form/SC%20TO-I%2FA", + wantQuery: map[string]string{"statuses": "open", "enrich": "false"}, + }, + { + name: "detail", + call: func() error { + _, err := client.GetSituation("sit/with spaces", map[string]string{"enrich": "false"}) + return err + }, + wantPath: "/v1/situations/sit%2Fwith%20spaces", + wantQuery: map[string]string{"enrich": "false"}, + }, + { + name: "filings", + call: func() error { + _, err := client.SituationFilings("sit/with spaces", map[string]string{"limit": "5", "cursor": "10"}) + return err + }, + wantPath: "/v1/situations/sit%2Fwith%20spaces/filings", + wantQuery: map[string]string{"limit": "5", "cursor": "10"}, + }, + { + name: "summary", + call: func() error { + _, err := client.SituationSummary("sit/with spaces", map[string]string{"response_mode": "compact"}) + return err + }, + wantPath: "/v1/situations/sit%2Fwith%20spaces/summary", + wantQuery: map[string]string{"response_mode": "compact"}, + }, + } + + for _, test := range calls { + if err := test.call(); err != nil { + t.Fatalf("%s failed: %v", test.name, err) + } + } + if len(*captured) != len(calls) { + t.Fatalf("captured %d requests, want %d", len(*captured), len(calls)) + } + for i, test := range calls { + request := (*captured)[i] + if request.Method != http.MethodGet { + t.Fatalf("%s method = %q, want GET", test.name, request.Method) + } + if request.Path != test.wantPath { + t.Fatalf("%s path = %q, want %q", test.name, request.Path, test.wantPath) + } + if strings.Contains(request.Path, "tikr") || strings.Contains(request.Path, "internal") { + t.Fatalf("%s path exposes an internal/provider namespace: %q", test.name, request.Path) + } + query, err := url.ParseQuery(request.Query) + if err != nil { + t.Fatalf("%s query parse failed: %v", test.name, err) + } + for key, value := range test.wantQuery { + if query.Get(key) != value { + t.Fatalf("%s query %s = %q, want %q in %q", test.name, key, query.Get(key), value, request.Query) + } + } + } +} + +func TestSituationServiceDelegatesToPublicRoutes(t *testing.T) { + client, captured, closeServer := newCaptureClient(t) + defer closeServer() + + calls := []func() error{ + func() error { _, err := client.Situations.Issues(map[string]string{"limit": "3"}); return err }, + func() error { + _, err := client.Situations.Issue("special-situations-digest-1-2026-07-10", nil) + return err + }, + func() error { _, err := client.Situations.Get("sit_123", nil); return err }, + func() error { + _, err := client.Situations.Filings("sit_123", map[string]string{"limit": "2"}) + return err + }, + func() error { _, err := client.Situations.Summary("sit_123", nil); return err }, + func() error { _, err := client.Situations.Feed(map[string]string{"limit": "2"}); return err }, + func() error { _, err := client.Situations.Calendar(map[string]string{"days": "30"}); return err }, + func() error { _, err := client.Situations.Stats(map[string]string{"window": "7d"}); return err }, + func() error { + _, err := client.Situations.Performance(map[string]string{"group_by": "type"}) + return err + }, + func() error { _, err := client.Situations.ByForm("8-K", map[string]string{"limit": "2"}); return err }, + } + + for _, call := range calls { + if err := call(); err != nil { + t.Fatalf("situation service call failed: %v", err) + } + } + + wantPaths := []string{ + "/v1/situations/issues", + "/v1/situations/issues/special-situations-digest-1-2026-07-10", + "/v1/situations/sit_123", + "/v1/situations/sit_123/filings", + "/v1/situations/sit_123/summary", + "/v1/situations/feed", + "/v1/situations/calendar", + "/v1/situations/stats", + "/v1/situations/performance", + "/v1/situations/by-form/8-K", + } + for i, want := range wantPaths { + if (*captured)[i].Path != want { + t.Fatalf("path %d = %q, want %q", i, (*captured)[i].Path, want) + } + } +} + +func TestExportSituationReturnsMarkdownText(t *testing.T) { + captured := []capturedRequest{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + captured = append(captured, capturedRequest{ + Method: r.Method, + Path: r.URL.EscapedPath(), + Query: r.URL.RawQuery, + Header: r.Header.Clone(), + }) + w.Header().Set("content-type", "text/markdown; charset=utf-8") + _, _ = w.Write([]byte("# Situation\n\nCopy-for-LLM export.")) + })) + defer server.Close() + + client := NewClient("") + client.BaseURL = server.URL + client.HTTPClient = server.Client() + + markdown, err := client.Situations.Export("sit/with spaces", map[string]string{"response_mode": "compact"}) + if err != nil { + t.Fatalf("ExportSituation failed: %v", err) + } + if markdown != "# Situation\n\nCopy-for-LLM export." { + t.Fatalf("markdown = %q, want raw text payload", markdown) + } + if captured[0].Path != "/v1/situations/sit%2Fwith%20spaces/export" { + t.Fatalf("path = %q, want escaped export route", captured[0].Path) + } + if captured[0].Method != http.MethodGet { + t.Fatalf("method = %q, want GET", captured[0].Method) + } + query, err := url.ParseQuery(captured[0].Query) + if err != nil { + t.Fatalf("parse export query: %v", err) + } + if query.Get("response_mode") != "compact" { + t.Fatalf("export query = %q, want response_mode=compact", captured[0].Query) + } +} + func TestFactorParityWrappersRouteToLaunchPaths(t *testing.T) { client, captured, closeServer := newCaptureClient(t) defer closeServer() From 5a8b97933a50f96b056204a69eaaecd1683d9f06 Mon Sep 17 00:00:00 2001 From: Hyperion <89154626+daniel-scrivner@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:26:17 -0600 Subject: [PATCH 2/2] feat: add underwriting pack SDK helper --- README.md | 22 ++++++++++++++++++---- client.go | 19 +++++++++++++++++++ client_test.go | 14 ++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f31a49f..4c7e955 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,8 @@ constructors before using grouped fields. ## Special Situations Special Situations helpers cover the public SEC-derived situations API: list, -detail, filings timeline, summary, Copy-for-LLM export, feed, calendar, stats, -performance, and EDGAR form lookup. +detail, filings timeline, summary, underwriting pack facade, Copy-for-LLM +export, feed, calendar, stats, performance, and EDGAR form lookup. ```go client := secapi.NewClient(os.Getenv("SECAPI_API_KEY")) @@ -174,13 +174,27 @@ if err != nil { panic(err) } fmt.Println(calendar["data"]) + +pack, err := client.Situations.UnderwritingPack("sit_123") +if err != nil { + panic(err) +} +fmt.Println(pack["summary"]) ``` Archive issue helpers read the immutable paid weekly issue archive: `client.Situations.Issues(...)` and `client.Situations.Issue(...)`, with flat aliases `ListSituationIssues` and `GetSituationIssue`. These archive endpoints -depend on unmerged datastream PR #1363, so they require an API deployment that -includes that server change before they will work against production. +depend on pending datastream PR #1363 deployment, so they require an API +deployment that includes that server change before they will work against +production. + +The underwriting pack facade is available as grouped +`client.Situations.UnderwritingPack(...)` or flat `client.SituationUnderwritingPack(...)`. +It calls `GET /v1/situations/{id}/underwriting-pack`, returns only the public +Special Situations facade payload, and does not expose internal/provider or TIKR +data. Like the archive issue endpoints, it requires an API deployment that +includes pending datastream PR #1363. ## Auto-pagination diff --git a/client.go b/client.go index ddbac9f..202bb1a 100644 --- a/client.go +++ b/client.go @@ -796,6 +796,14 @@ func (s *SituationService) SummaryWithContext(ctx context.Context, situationID s return s.client.SituationSummaryWithContext(ctx, situationID, params) } +func (s *SituationService) UnderwritingPack(situationID string) (map[string]any, error) { + return s.client.SituationUnderwritingPack(situationID) +} + +func (s *SituationService) UnderwritingPackWithContext(ctx context.Context, situationID string) (map[string]any, error) { + return s.client.SituationUnderwritingPackWithContext(ctx, situationID) +} + func (s *SituationService) Export(situationID string, params map[string]string) (string, error) { return s.client.ExportSituation(situationID, params) } @@ -1604,6 +1612,17 @@ func (c *Client) SituationSummaryWithContext(ctx context.Context, situationID st return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/"+url.PathEscape(situationID)+"/summary", params, nil) } +// SituationUnderwritingPack returns the public Special Situations underwriting +// facade for a situation. The endpoint requires an API deployment with +// datastream PR #1363. +func (c *Client) SituationUnderwritingPack(situationID string) (map[string]any, error) { + return c.SituationUnderwritingPackWithContext(context.Background(), situationID) +} + +func (c *Client) SituationUnderwritingPackWithContext(ctx context.Context, situationID string) (map[string]any, error) { + return c.requestWithContext(ctx, http.MethodGet, "/v1/situations/"+url.PathEscape(situationID)+"/underwriting-pack", nil, nil) +} + func (c *Client) ExportSituation(situationID string, params map[string]string) (string, error) { return c.ExportSituationWithContext(context.Background(), situationID, params) } diff --git a/client_test.go b/client_test.go index ebf0b61..3bfe6b3 100644 --- a/client_test.go +++ b/client_test.go @@ -1110,6 +1110,15 @@ func TestSituationWrappersRouteToPublicSituationPaths(t *testing.T) { wantPath: "/v1/situations/sit%2Fwith%20spaces/summary", wantQuery: map[string]string{"response_mode": "compact"}, }, + { + name: "underwriting pack", + call: func() error { + _, err := client.SituationUnderwritingPack("sit/with spaces") + return err + }, + wantPath: "/v1/situations/sit%2Fwith%20spaces/underwriting-pack", + wantQuery: nil, + }, } for _, test := range calls { @@ -1159,6 +1168,10 @@ func TestSituationServiceDelegatesToPublicRoutes(t *testing.T) { return err }, func() error { _, err := client.Situations.Summary("sit_123", nil); return err }, + func() error { + _, err := client.Situations.UnderwritingPackWithContext(context.Background(), "sit/needs underwriting") + return err + }, func() error { _, err := client.Situations.Feed(map[string]string{"limit": "2"}); return err }, func() error { _, err := client.Situations.Calendar(map[string]string{"days": "30"}); return err }, func() error { _, err := client.Situations.Stats(map[string]string{"window": "7d"}); return err }, @@ -1181,6 +1194,7 @@ func TestSituationServiceDelegatesToPublicRoutes(t *testing.T) { "/v1/situations/sit_123", "/v1/situations/sit_123/filings", "/v1/situations/sit_123/summary", + "/v1/situations/sit%2Fneeds%20underwriting/underwriting-pack", "/v1/situations/feed", "/v1/situations/calendar", "/v1/situations/stats",