Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 45f8965

Browse files
authored
Merge pull request #66 from philips-software/feature/ai
AI clients
2 parents b431ed9 + 137ad05 commit 45f8965

25 files changed

+2483
-15
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ jobs:
1919
with:
2020
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
2121
version: v1.29
22+
args: --timeout 5m0s

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,17 @@ The current implement covers only a subset of HSDP APIs. Basically we implement
5656
- [x] Data Type Definitions
5757
- [x] Label Definitions
5858
- [x] Export Routes
59-
59+
- [x] AI Inference
60+
- [x] Compute Environment management
61+
- [x] Compute Target managements
62+
- [x] Model management
63+
- [x] Inference Job management
64+
- [x] AI Training
65+
- [x] Compute Environment management
66+
- [x] Model management
67+
- [x] AI Workspace
68+
- [x] Compute Target management
69+
- [x] Workspace management
6070

6171
## Usage
6272

ai/client.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Package ai provides support the HSDP AI services
2+
package ai
3+
4+
import (
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"io/ioutil"
10+
"net/http"
11+
"net/url"
12+
"os"
13+
"path"
14+
"strings"
15+
16+
"github.com/go-playground/validator/v10"
17+
"github.com/google/go-querystring/query"
18+
autoconf "github.com/philips-software/go-hsdp-api/config"
19+
"github.com/philips-software/go-hsdp-api/iam"
20+
"github.com/philips-software/go-hsdp-api/internal"
21+
)
22+
23+
const (
24+
userAgent = "go-hsdp-api/ai/" + internal.LibraryVersion
25+
APIVersion = "1"
26+
)
27+
28+
// OptionFunc is the function signature function for options
29+
type OptionFunc func(*http.Request) error
30+
31+
// Config contains the configuration of a Client
32+
type Config struct {
33+
Region string
34+
Environment string
35+
OrganizationID string `Validate:"required"`
36+
AnalyzeURL string
37+
Service string `Validate:"required"`
38+
DebugLog string
39+
Retry int
40+
}
41+
42+
// A Client manages communication with HSDP AI Analyze API
43+
type Client struct {
44+
// HTTP Client used to communicate with IAM API
45+
iamClient *iam.Client
46+
config *Config
47+
analyzeURL *url.URL
48+
49+
// User agent used when communicating with the HSDP Notification API
50+
UserAgent string
51+
52+
debugFile *os.File
53+
validate *validator.Validate
54+
55+
ComputeTarget *ComputeTargetService
56+
ComputeProvider *ComputeProviderService
57+
}
58+
59+
// NewClient returns a new AI base Client
60+
func NewClient(iamClient *iam.Client, config *Config) (*Client, error) {
61+
validate := validator.New()
62+
if err := validate.Struct(config); err != nil {
63+
return nil, err
64+
}
65+
doAutoconf(config)
66+
c := &Client{iamClient: iamClient, config: config, UserAgent: userAgent, validate: validator.New()}
67+
68+
if err := c.SetAnalyzeURL(config.AnalyzeURL); err != nil {
69+
return nil, err
70+
}
71+
72+
c.ComputeProvider = &ComputeProviderService{client: c, validate: validator.New()}
73+
c.ComputeTarget = &ComputeTargetService{client: c, validate: validator.New()}
74+
75+
return c, nil
76+
}
77+
78+
func doAutoconf(config *Config) {
79+
if config.Region != "" && config.Environment != "" {
80+
c, err := autoconf.New(
81+
autoconf.WithRegion(config.Region),
82+
autoconf.WithEnv(config.Environment))
83+
if err == nil {
84+
analyzeService := c.Service(config.Service)
85+
if analyzeService.URL != "" && config.AnalyzeURL == "" {
86+
config.AnalyzeURL = analyzeService.URL
87+
}
88+
}
89+
}
90+
}
91+
92+
// Close releases allocated resources of clients
93+
func (c *Client) Close() {
94+
if c.debugFile != nil {
95+
_ = c.debugFile.Close()
96+
c.debugFile = nil
97+
}
98+
}
99+
100+
// GetAnalyzeURL returns the base CDL Store base URL as configured
101+
func (c *Client) GetAnalyzeURL() string {
102+
if c.analyzeURL == nil {
103+
return ""
104+
}
105+
return c.analyzeURL.String()
106+
}
107+
108+
// SetAnalyzeURL sets the Notification URL for API requests
109+
func (c *Client) SetAnalyzeURL(urlStr string) error {
110+
if urlStr == "" {
111+
return ErrAnalyzeURLCannotBeEmpty
112+
}
113+
// Make sure the given URL end with a slash
114+
if !strings.HasSuffix(urlStr, "/") {
115+
urlStr += "/"
116+
}
117+
var err error
118+
c.analyzeURL, err = url.Parse(urlStr)
119+
return err
120+
}
121+
122+
// GetEndpointURL returns the FHIR Store Endpoint URL as configured
123+
func (c *Client) GetEndpointURL() string {
124+
return c.GetAnalyzeURL() + path.Join("analyze", c.config.Service, c.config.OrganizationID)
125+
}
126+
127+
// SetEndpointURL sets the endpoint URL for API requests to a custom endpoint. urlStr
128+
// should always be specified with a trailing slash.
129+
func (c *Client) SetEndpointURL(urlStr string) error {
130+
if urlStr == "" {
131+
return ErrAnalyzeURLCannotBeEmpty
132+
}
133+
// Make sure the given URL ends with a slash
134+
if !strings.HasSuffix(urlStr, "/") {
135+
urlStr += "/"
136+
}
137+
var err error
138+
c.analyzeURL, err = url.Parse(urlStr)
139+
if err != nil {
140+
return err
141+
}
142+
parts := strings.Split(c.analyzeURL.Path, "/")
143+
if len(parts) == 0 {
144+
return ErrAnalyzeURLCannotBeEmpty
145+
}
146+
if len(parts) < 5 {
147+
return ErrInvalidEndpointURL
148+
}
149+
c.config.OrganizationID = parts[len(parts)-2]
150+
c.analyzeURL.Path = "/"
151+
return nil
152+
}
153+
154+
func (c *Client) NewAIRequest(method, requestPath string, opt interface{}, options ...OptionFunc) (*http.Request, error) {
155+
u := *c.analyzeURL
156+
// Set the encoded opaque data
157+
u.Opaque = path.Join(c.analyzeURL.Path, "analyze", c.config.Service, c.config.OrganizationID, requestPath)
158+
159+
if opt != nil {
160+
q, err := query.Values(opt)
161+
if err != nil {
162+
return nil, err
163+
}
164+
u.RawQuery = q.Encode()
165+
}
166+
167+
req := &http.Request{
168+
Method: method,
169+
URL: &u,
170+
Proto: "HTTP/1.1",
171+
ProtoMajor: 1,
172+
ProtoMinor: 1,
173+
Header: make(http.Header),
174+
Host: u.Host,
175+
}
176+
if opt != nil {
177+
q, err := query.Values(opt)
178+
if err != nil {
179+
return nil, err
180+
}
181+
u.RawQuery = q.Encode()
182+
}
183+
184+
if method == "POST" || method == "PUT" {
185+
bodyBytes, err := json.Marshal(opt)
186+
if err != nil {
187+
return nil, err
188+
}
189+
bodyReader := bytes.NewReader(bodyBytes)
190+
191+
u.RawQuery = ""
192+
req.Body = ioutil.NopCloser(bodyReader)
193+
req.ContentLength = int64(bodyReader.Len())
194+
req.Header.Set("Content-Type", "application/json")
195+
}
196+
req.Header.Set("Accept", "*/*")
197+
req.Header.Set("Authorization", "Bearer "+c.iamClient.Token())
198+
req.Header.Set("API-Version", APIVersion)
199+
if c.UserAgent != "" {
200+
req.Header.Set("User-Agent", c.UserAgent)
201+
}
202+
for _, fn := range options {
203+
if fn == nil {
204+
continue
205+
}
206+
if err := fn(req); err != nil {
207+
return nil, err
208+
}
209+
}
210+
return req, nil
211+
}
212+
213+
// Response is a HSDP IAM API response. This wraps the standard http.Response
214+
// returned from HSDP IAM and provides convenient access to things like errors
215+
type Response struct {
216+
*http.Response
217+
}
218+
219+
// newResponse creates a new Response for the provided http.Response.
220+
func newResponse(r *http.Response) *Response {
221+
response := &Response{Response: r}
222+
return response
223+
}
224+
225+
// TokenRefresh forces a refresh of the IAM access token
226+
func (c *Client) TokenRefresh() error {
227+
if c.iamClient == nil {
228+
return fmt.Errorf("invalid IAM Client, cannot refresh token")
229+
}
230+
return c.iamClient.TokenRefresh()
231+
}
232+
233+
// Do executes a http request. If v implements the io.Writer
234+
// interface, the raw response body will be written to v, without attempting to
235+
// first decode it.
236+
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
237+
resp, err := c.iamClient.HttpClient().Do(req)
238+
if err != nil {
239+
return nil, err
240+
}
241+
242+
response := newResponse(resp)
243+
244+
err = internal.CheckResponse(resp)
245+
if err != nil {
246+
// even though there was an error, we still return the response
247+
// in case the caller wants to inspect it further
248+
return response, err
249+
}
250+
251+
if v != nil {
252+
defer resp.Body.Close() // Only close if we plan to read it
253+
if w, ok := v.(io.Writer); ok {
254+
_, err = io.Copy(w, resp.Body)
255+
} else {
256+
err = json.NewDecoder(resp.Body).Decode(v)
257+
}
258+
}
259+
260+
return response, err
261+
}

0 commit comments

Comments
 (0)