diff --git a/README.md b/README.md index 35782a0..d38925b 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ | [ripper](#ripper) | change runs status | 🦍 | | ✅ | | [scalp](#scalp) | incremental scoring | | 🦍 | ✅ | | [valeria](#valeria) | valuer.cfg + tex scoring | | 🦍 | ✅ | -| 👻 | list/commit problems | | 🦍 | 🧑‍💻 | -| 👻 | regexp problem upload | | 🦍 | 🤔 | +| [wooda](#wooda) | regexp problem upload | | 🦍 | 🧑‍💻 | +| ⚙️ | move json config to ini | | | 🧑‍💻 | +| 👻 | list/commit problems | | 🦍 | 🤔 | | 👻 | download/upload package | | 🦍 | 🤔 | | 👻 | import polygon problem | 🦍 | 🦍 | 🤔 | | 👻 | autogen static problem | 🦍 | | 🤔 | @@ -24,6 +25,7 @@ - 🧑‍💻 In progress - 🤔 To do - 👻 Name placeholder +- ⚙️ Refactor task - 🦍 Engines usage ## Build @@ -48,7 +50,13 @@ Put your config file in `~/.config/algolymp/config.json`. "polygon": { "url": "https://polygon.codeforces.com", "apiKey": "", - "apiSecret": "" + "apiSecret": "", + "wooda": { + "polygon": { + "ignore": ".*\\.a$", + "test": "^tests/\\d+$" + } + } }, "system": { "editor": "nano" @@ -269,3 +277,37 @@ valeria -i 318882 | bat -l tex ``` ![valeria logo](https://algolymp.ru/static/img/valeria.png) + +## wooda +*Upload problem files filtered by regexp to Polygon using API.* + +### About + +**Now this is a proof of concept. Many more mods will be supported in the future.** + +Match all files in directory with config regexp patterns. Upload recognized files to Polygon. + +Supported modes: + +- `ignore` +- `test` + +### Flags +- `-i` - problem id (required) +- `-m` - mode from config (required) +- `-d` - problem directory (required) + +### Config +- `polygon.url` +- `polygon.apiKey` +- `polygon.apiSecret` +- `wooda` + +### Examples + +```bash +wooda --help +wooda -i 337320 -m polygon -d . +``` + +![wooda logo](https://algolymp.ru/static/img/wooda.png) diff --git a/cmd/wooda/main.go b/cmd/wooda/main.go new file mode 100644 index 0000000..d632f72 --- /dev/null +++ b/cmd/wooda/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/Gornak40/algolymp/config" + "github.com/Gornak40/algolymp/polygon" + "github.com/Gornak40/algolymp/polygon/wooda" + "github.com/akamensky/argparse" + "github.com/sirupsen/logrus" +) + +func main() { + cfg := config.NewConfig() + woodaKeys := make([]string, 0, len(cfg.Polygon.Wooda)) + for k := range cfg.Polygon.Wooda { + woodaKeys = append(woodaKeys, k) + } + + parser := argparse.NewParser("wooda", "Upload problem files filtered by regexp to Polygon.") + pID := parser.Int("i", "pid", &argparse.Options{ + Required: true, + Help: "Polygon problem ID", + }) + mode := parser.Selector("m", "mode", woodaKeys, &argparse.Options{ + Required: true, + Help: "Local storage mode", + }) + pDir := parser.String("d", "directory", &argparse.Options{ + Required: true, + Help: "Local storage directory", + }) + if err := parser.Parse(os.Args); err != nil { + logrus.WithError(err).Fatal("bad arguments") + } + + pClient := polygon.NewPolygon(&cfg.Polygon) + woodaCfg := cfg.Polygon.Wooda[*mode] // mode is good argparse.Selector + wooda := wooda.NewWooda(pClient, *pID, &woodaCfg) + if err := filepath.Walk(*pDir, wooda.DirWalker); err != nil { + logrus.WithError(err).Fatal("failed wooda matching") + } +} + +/* +"wooda": { + "polygon": { + "ignore": ["tests/*\.a"], + "test": ["tests/*"], + "validator": "files/val*\.cpp", + "checker": "checker\.cpp", + "solution": ["solutions/*\.cpp"], + "generator": ["files/gen*\.cpp"] + } +} +*/ diff --git a/logos/wooda.png b/logos/wooda.png new file mode 100644 index 0000000..b9e252f Binary files /dev/null and b/logos/wooda.png differ diff --git a/polygon/answers.go b/polygon/answers.go new file mode 100644 index 0000000..5c34acc --- /dev/null +++ b/polygon/answers.go @@ -0,0 +1,23 @@ +package polygon + +import "encoding/json" + +type TestAnswer struct { + Index int `json:"index"` + Group string `json:"group"` + Points float32 `json:"points"` + UseInStatements bool `json:"useInStatements"` +} + +type GroupAnswer struct { + Name string `json:"name"` + PointsPolicy string `json:"pointsPolicy"` + FeedbackPolicy string `json:"feedbackPolicy"` + Dependencies []string `json:"dependencies"` +} + +type Answer struct { + Status string `json:"status"` + Comment string `json:"comment"` + Result json.RawMessage `json:"result"` +} diff --git a/polygon/api.go b/polygon/api.go index e23fe34..ded99f5 100644 --- a/polygon/api.go +++ b/polygon/api.go @@ -25,12 +25,19 @@ const ( var ( ErrBadPolygonStatus = errors.New("bad polygon status") + ErrInvalidMethod = errors.New("invalid method") ) type Config struct { - URL string `json:"url"` - APIKey string `json:"apiKey"` - APISecret string `json:"apiSecret"` + URL string `json:"url"` + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` + Wooda map[string]WoodaConfig `json:"wooda"` +} + +type WoodaConfig struct { + Ignore string `json:"ignore"` + Test string `json:"test"` } type Polygon struct { @@ -47,8 +54,36 @@ func NewPolygon(cfg *Config) *Polygon { } } -func (p *Polygon) makeQuery(method string, link string) (*Answer, error) { - req, _ := http.NewRequestWithContext(context.TODO(), method, link, nil) +func buildRequest(method, link string, params url.Values) (*http.Request, error) { + logrus.WithFields(logrus.Fields{ + "method": method, + "url": link, + }).Info("build request") + + switch method { + case http.MethodGet: + link = fmt.Sprintf("%s?%s", link, params.Encode()) + + return http.NewRequestWithContext(context.TODO(), method, link, nil) + case http.MethodPost: + buf := strings.NewReader(params.Encode()) + req, err := http.NewRequestWithContext(context.TODO(), method, link, buf) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + return req, nil + default: + return nil, ErrInvalidMethod + } +} + +func (p *Polygon) makeQuery(method, link string, params url.Values) (*Answer, error) { + req, err := buildRequest(method, link, params) + if err != nil { + return nil, err + } resp, err := p.client.Do(req) if err != nil { return nil, err @@ -81,9 +116,8 @@ func (p *Polygon) skipEscape(params url.Values) string { return strings.Join(pairs, "&") } -func (p *Polygon) buildURL(method string, params url.Values) string { +func (p *Polygon) buildURL(method string, params url.Values) (string, url.Values) { url, _ := url.JoinPath(p.cfg.URL, "api", method) - logrus.WithField("method", method).Info("preparing the request") params["apiKey"] = []string{p.cfg.APIKey} params["time"] = []string{strconv.FormatInt(time.Now().Unix(), 10)} @@ -93,35 +127,15 @@ func (p *Polygon) buildURL(method string, params url.Values) string { hsh := hex.EncodeToString(b[:]) params["apiSig"] = []string{sixSecretSymbols + hsh} - return fmt.Sprintf("%s?%s", url, params.Encode()) -} - -type TestAnswer struct { - Index int `json:"index"` - Group string `json:"group"` - Points float32 `json:"points"` - UseInStatements bool `json:"useInStatements"` -} - -type GroupAnswer struct { - Name string `json:"name"` - PointsPolicy string `json:"pointsPolicy"` - FeedbackPolicy string `json:"feedbackPolicy"` - Dependencies []string `json:"dependencies"` -} - -type Answer struct { - Status string `json:"status"` - Comment string `json:"comment"` - Result json.RawMessage `json:"result"` + return url, params } func (p *Polygon) GetGroups(pID int) ([]GroupAnswer, error) { - link := p.buildURL("problem.viewTestGroup", url.Values{ + link, params := p.buildURL("problem.viewTestGroup", url.Values{ "problemId": []string{strconv.Itoa(pID)}, "testset": []string{defaultTestset}, }) - ansG, err := p.makeQuery(http.MethodGet, link) + ansG, err := p.makeQuery(http.MethodGet, link, params) if err != nil { return nil, err } @@ -132,12 +146,12 @@ func (p *Polygon) GetGroups(pID int) ([]GroupAnswer, error) { } func (p *Polygon) GetTests(pID int) ([]TestAnswer, error) { - link := p.buildURL("problem.tests", url.Values{ + link, params := p.buildURL("problem.tests", url.Values{ "problemId": []string{strconv.Itoa(pID)}, "testset": []string{defaultTestset}, "noInputs": []string{"true"}, }) - ansT, err := p.makeQuery(http.MethodGet, link) + ansT, err := p.makeQuery(http.MethodGet, link, params) if err != nil { return nil, err } @@ -148,51 +162,41 @@ func (p *Polygon) GetTests(pID int) ([]TestAnswer, error) { } func (p *Polygon) EnableGroups(pID int) error { - link := p.buildURL("problem.enableGroups", url.Values{ + link, params := p.buildURL("problem.enableGroups", url.Values{ "problemId": []string{strconv.Itoa(pID)}, "testset": []string{defaultTestset}, "enable": []string{"true"}, }) - _, err := p.makeQuery(http.MethodPost, link) + _, err := p.makeQuery(http.MethodPost, link, params) return err } func (p *Polygon) EnablePoints(pID int) error { - link := p.buildURL("problem.enablePoints", url.Values{ + link, params := p.buildURL("problem.enablePoints", url.Values{ "problemId": []string{strconv.Itoa(pID)}, "enable": []string{"true"}, }) - _, err := p.makeQuery(http.MethodPost, link) + _, err := p.makeQuery(http.MethodPost, link, params) return err } -type TestRequest url.Values - -func NewTestRequest(pID int, index int) TestRequest { - return TestRequest{ +func (p *Polygon) SaveResource(pID int, name, content string) error { + link, params := p.buildURL("problem.saveFile", url.Values{ "problemId": []string{strconv.Itoa(pID)}, - "testIndex": []string{strconv.Itoa(index)}, - "testset": []string{defaultTestset}, - } -} - -func (tr TestRequest) Group(group string) TestRequest { - tr["testGroup"] = []string{group} - - return tr -} - -func (tr TestRequest) Points(points float32) TestRequest { - tr["testPoints"] = []string{fmt.Sprint(points)} + "type": []string{"resource"}, + "name": []string{name}, + "file": []string{content}, + }) + _, err := p.makeQuery(http.MethodPost, link, params) - return tr + return err } func (p *Polygon) SaveTest(tReq TestRequest) error { - link := p.buildURL("problem.saveTest", url.Values(tReq)) - _, err := p.makeQuery(http.MethodPost, link) + link, params := p.buildURL("problem.saveTest", url.Values(tReq)) + _, err := p.makeQuery(http.MethodPost, link, params) return err } diff --git a/polygon/requests.go b/polygon/requests.go new file mode 100644 index 0000000..107bc6b --- /dev/null +++ b/polygon/requests.go @@ -0,0 +1,41 @@ +package polygon + +import ( + "fmt" + "net/url" + "strconv" +) + +type TestRequest url.Values + +func NewTestRequest(pID int, index int) TestRequest { + return TestRequest{ + "problemId": []string{strconv.Itoa(pID)}, + "testIndex": []string{strconv.Itoa(index)}, + "testset": []string{defaultTestset}, + } +} + +func (tr TestRequest) Group(group string) TestRequest { + tr["testGroup"] = []string{group} + + return tr +} + +func (tr TestRequest) Points(points float32) TestRequest { + tr["testPoints"] = []string{fmt.Sprint(points)} + + return tr +} + +func (tr TestRequest) Input(input string) TestRequest { + tr["testInput"] = []string{input} + + return tr +} + +func (tr TestRequest) Description(description string) TestRequest { + tr["testDescription"] = []string{description} + + return tr +} diff --git a/polygon/scoring.go b/polygon/scoring.go index 3136908..e0be010 100644 --- a/polygon/scoring.go +++ b/polygon/scoring.go @@ -3,9 +3,6 @@ package polygon import ( "errors" "fmt" - "net/http" - "net/url" - "strconv" "strings" "github.com/sirupsen/logrus" @@ -121,14 +118,7 @@ func (p *Polygon) InformaticsValuer(pID int, verbose bool) error { if verbose { logrus.Info("valuer.cfg\n" + valuer) } - - link := p.buildURL("problem.saveFile", url.Values{ - "problemId": []string{strconv.Itoa(pID)}, - "type": []string{"resource"}, - "name": []string{"valuer.cfg"}, - "file": []string{valuer}, - }) - if _, err := p.makeQuery(http.MethodPost, link); err != nil { + if err := p.SaveResource(pID, "valuer.cfg", valuer); err != nil { return err } diff --git a/polygon/wooda/wooda.go b/polygon/wooda/wooda.go new file mode 100644 index 0000000..3ef1cdd --- /dev/null +++ b/polygon/wooda/wooda.go @@ -0,0 +1,92 @@ +package wooda + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + + "github.com/Gornak40/algolymp/polygon" + "github.com/sirupsen/logrus" +) + +type Wooda struct { + client *polygon.Polygon + pID int + config *polygon.WoodaConfig + testIndex int +} + +func NewWooda(pClient *polygon.Polygon, pID int, wCfg *polygon.WoodaConfig) *Wooda { + return &Wooda{ + client: pClient, + pID: pID, + config: wCfg, + testIndex: 1, + } +} + +func pathMatch(pattern, path string) bool { + res, err := regexp.MatchString(pattern, path) + if err != nil { + logrus.WithError(err).Error("failed match filepath") + + return false + } + + return res +} + +func getData(mode, path string) (string, error) { + logrus.WithFields(logrus.Fields{"mode": mode, "path": path}).Info("resolve file") + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + + return string(data), nil +} + +func (w *Wooda) resolveTest(path string) error { + data, err := getData("test", path) + if err != nil { + return err + } + + tr := polygon.NewTestRequest(w.pID, w.testIndex). + Input(data). + Description(fmt.Sprintf("File \"%s\"", filepath.Base(path))) + if err := w.client.SaveTest(tr); err != nil { + return err + } + w.testIndex++ + + return nil +} + +func (w *Wooda) matcher(path string) error { + switch { + case pathMatch(w.config.Ignore, path): // silent ignore is the best practice + break + case pathMatch(w.config.Test, path): + if err := w.resolveTest(path); err != nil { + return err + } + default: + logrus.WithField("path", path).Warn("no valid matching") + } + + return nil +} + +func (w *Wooda) DirWalker(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + return w.matcher(path) +}