-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
allow scientific price notation #3253
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -497,3 +497,20 @@ func MimeTypeToExtension(mimeType string) (string, error) { | |
} | ||
return "", ErrNoExtensionsForType | ||
} | ||
|
||
// ParsePricePerUnit parses a price string in the format <price><exponent><currency> and returns the price as a big.Rat and the currency. | ||
func ParsePricePerUnit(pricePerUnitStr string) (*big.Rat, string, error) { | ||
pricePerUnitRex := regexp.MustCompile(`^(\d+(\.\d+)?([eE][+-]?\d+)?)([A-Za-z][A-Za-z0-9]*)?$`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this change backward-compatible? I see you use a different regex here, so I wonder, won't it break anything for the existing config? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From a quick glimpse, it seems like the incompatibility would be if we had a currency being specified as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I'm fine even with some "slight" non-backward-compatibility as long as it's ok with Orchestrators. |
||
match := pricePerUnitRex.FindStringSubmatch(pricePerUnitStr) | ||
if match == nil { | ||
return nil, "", fmt.Errorf("price must be in the format of <price><exponent><currency>, provided %v", pricePerUnitStr) | ||
} | ||
price, currency := match[1], match[4] | ||
|
||
pricePerUnit, ok := new(big.Rat).SetString(price) | ||
if !ok { | ||
return nil, "", fmt.Errorf("price must be a valid number, provided %v", match[1]) | ||
} | ||
|
||
return pricePerUnit, currency, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ import ( | |
"github.com/livepeer/go-livepeer/net" | ||
"github.com/livepeer/lpms/ffmpeg" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestFFmpegProfiletoNetProfile(t *testing.T) { | ||
|
@@ -476,3 +477,119 @@ func TestMimeTypeToExtension(t *testing.T) { | |
_, err := MimeTypeToExtension(invalidContentType) | ||
assert.Equal(ErrNoExtensionsForType, err) | ||
} | ||
|
||
func TestParsePricePerUnit(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
pricePerUnitStr string | ||
expectedPrice *big.Rat | ||
expectedCurrency string | ||
expectError bool | ||
}{ | ||
{ | ||
name: "Valid input with integer price", | ||
pricePerUnitStr: "100USD", | ||
expectedPrice: big.NewRat(100, 1), | ||
expectedCurrency: "USD", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Valid input with fractional price", | ||
pricePerUnitStr: "0.13USD", | ||
expectedPrice: big.NewRat(13, 100), | ||
expectedCurrency: "USD", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Valid input with decimal price", | ||
pricePerUnitStr: "99.99EUR", | ||
expectedPrice: big.NewRat(9999, 100), | ||
expectedCurrency: "EUR", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Lower case currency", | ||
pricePerUnitStr: "99.99eur", | ||
expectedPrice: big.NewRat(9999, 100), | ||
expectedCurrency: "eur", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Currency with numbers", | ||
pricePerUnitStr: "420DOG3", | ||
expectedPrice: big.NewRat(420, 1), | ||
expectedCurrency: "DOG3", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "No specified currency, empty currency", | ||
pricePerUnitStr: "100", | ||
expectedPrice: big.NewRat(100, 1), | ||
expectedCurrency: "", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Explicit wei currency", | ||
pricePerUnitStr: "100wei", | ||
expectedPrice: big.NewRat(100, 1), | ||
expectedCurrency: "wei", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Valid price with scientific notation and currency", | ||
pricePerUnitStr: "1.23e2USD", | ||
expectedPrice: big.NewRat(123, 1), | ||
expectedCurrency: "USD", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Valid price with capital scientific notation and currency", | ||
pricePerUnitStr: "1.23E2USD", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think having a space after the exponential notation would be helpful |
||
expectedPrice: big.NewRat(123, 1), | ||
expectedCurrency: "USD", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Valid price with negative scientific notation and currency", | ||
pricePerUnitStr: "1.23e-2USD", | ||
expectedPrice: big.NewRat(123, 10000), | ||
expectedCurrency: "USD", | ||
expectError: false, | ||
}, | ||
{ | ||
name: "Invalid number", | ||
pricePerUnitStr: "abcUSD", | ||
expectedPrice: nil, | ||
expectedCurrency: "", | ||
expectError: true, | ||
}, | ||
{ | ||
name: "Negative price", | ||
pricePerUnitStr: "-100USD", | ||
expectedPrice: nil, | ||
expectedCurrency: "", | ||
expectError: true, | ||
}, | ||
{ | ||
name: "Only exponent part without base (e-2)", | ||
pricePerUnitStr: "e-2USD", | ||
expectedPrice: nil, | ||
expectedCurrency: "", | ||
expectError: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
price, currency, err := ParsePricePerUnit(tt.pricePerUnitStr) | ||
|
||
if tt.expectError { | ||
assert.Error(t, err) | ||
} else { | ||
require.NoError(t, err) | ||
assert.True(t, tt.expectedPrice.Cmp(price) == 0) | ||
assert.Equal(t, tt.expectedCurrency, currency) | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import ( | |
|
||
"github.com/golang/glog" | ||
"github.com/livepeer/ai-worker/worker" | ||
"github.com/livepeer/go-livepeer/common" | ||
) | ||
|
||
var errPipelineNotAvailable = errors.New("pipeline not available") | ||
|
@@ -82,9 +83,51 @@ type AIModelConfig struct { | |
Currency string `json:"currency,omitempty"` | ||
} | ||
|
||
// UnmarshalJSON allows `PricePerUnit` to be specified as a string. | ||
func (s *AIModelConfig) UnmarshalJSON(data []byte) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you cover different scenarios here, like |
||
type Alias AIModelConfig | ||
aux := &struct { | ||
PricePerUnit interface{} `json:"price_per_unit"` | ||
*Alias | ||
}{ | ||
Alias: (*Alias)(s), | ||
} | ||
|
||
if err := json.Unmarshal(data, &aux); err != nil { | ||
return err | ||
} | ||
|
||
// Handle PricePerUnit | ||
var price JSONRat | ||
switch v := aux.PricePerUnit.(type) { | ||
case string: | ||
pricePerUnit, currency, err := common.ParsePricePerUnit(v) | ||
if err != nil { | ||
return fmt.Errorf("error parsing price_per_unit: %v", err) | ||
} | ||
price = JSONRat{pricePerUnit} | ||
if s.Currency == "" { | ||
s.Currency = currency | ||
} | ||
Comment on lines
+109
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I see what you are doing there. Is it worth having this logic compared to just keeping the currency in a separate field in the JSON? This provides multiple ways of doing the same thing and leads to ambiguous behavior (e.g. what happens if I specify both the AFAIK the go JSON lib (and the If we do go with the new way of specifying the price, I believe we should implement it for the other price config JSON's as well, like the video gateway for price per O. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking further about this, maybe we remove this change on the JSON config, which is redundant, and make it only for the CLI flags, wdyt? |
||
default: | ||
pricePerUnitData, err := json.Marshal(aux.PricePerUnit) | ||
if err != nil { | ||
return fmt.Errorf("error marshaling price_per_unit: %v", err) | ||
} | ||
if err := price.UnmarshalJSON(pricePerUnitData); err != nil { | ||
return fmt.Errorf("error unmarshaling price_per_unit: %v", err) | ||
} | ||
} | ||
s.PricePerUnit = price | ||
|
||
return nil | ||
} | ||
|
||
// ParseAIModelConfigs parses AI model configs from a file or a comma-separated list. | ||
func ParseAIModelConfigs(config string) ([]AIModelConfig, error) { | ||
var configs []AIModelConfig | ||
|
||
// Handle config files. | ||
info, err := os.Stat(config) | ||
if err == nil && !info.IsDir() { | ||
data, err := os.ReadFile(config) | ||
|
@@ -99,6 +142,7 @@ func ParseAIModelConfigs(config string) ([]AIModelConfig, error) { | |
return configs, nil | ||
} | ||
|
||
// Handle comma-separated list of model configs. | ||
models := strings.Split(config, ",") | ||
for _, m := range models { | ||
parts := strings.Split(m, ":") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@leszko, @ad-astra-video I noticed I forgot this in the last pull request.