Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
with:
submodules: true
# Sitemap lastmod comes from the latest content commit.
fetch-depth: 0

- name: Checkout tago
uses: actions/checkout@v6.0.2
uses: actions/checkout@v7.0.0
with:
repository: tamnd/tago
path: .tago-src
Expand Down Expand Up @@ -107,7 +107,7 @@ jobs:
group: cloudflare-pages-chemrxiv-cli
cancel-in-progress: true
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/checkout@v7.0.0
with:
fetch-depth: 1
sparse-checkout: scripts/
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:

# Tools GoReleaser shells out to for signing and SBOMs.
- uses: sigstore/cosign-installer@v3
- uses: anchore/sbom-action/download-syft@v0
- uses: anchore/sbom-action/download-syft@v0.24.0

- uses: goreleaser/goreleaser-action@v6
with:
Expand Down
21 changes: 16 additions & 5 deletions chemrxiv/chemrxiv.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,20 @@ func (c *Client) graphqlEndpoint() string {
// Search fetches preprints matching term. An empty term returns the most
// recent preprints. Returns up to limit results and the server total count.
func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint, int, error) {
return c.SearchFrom(ctx, term, 0, limit)
}

// SearchFrom fetches preprints matching term starting from the given skip
// offset. Returns up to limit results and the server total count.
func (c *Client) SearchFrom(ctx context.Context, term string, skip, limit int) ([]Preprint, int, error) {
if limit <= 0 {
limit = 20
}

const pageSize = 25
var out []Preprint
total := 0
skip := 0
cursor := skip

for {
fetch := pageSize
Expand All @@ -102,7 +108,7 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint

q := fmt.Sprintf(
`{ itemsByKeyword(skip:%d, limit:%d, term:%s) { totalCount itemHits { item { id doi title statusDate publishedDate version authors { firstName lastName institutions { name } } categories { name } keywords license metrics { viewCount downloadCount } asset { original { url } } } } } }`,
skip, fetch, jsonString(term),
cursor, fetch, jsonString(term),
)

var resp graphqlResponse[searchData]
Expand All @@ -117,7 +123,7 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint
total = result.TotalCount

for _, hit := range result.ItemHits {
out = append(out, wireToPreprint(hit.Item, len(out)+1))
out = append(out, wireToPreprint(hit.Item, skip+len(out)+1))
if len(out) >= limit {
return out, total, nil
}
Expand All @@ -126,14 +132,19 @@ func (c *Client) Search(ctx context.Context, term string, limit int) ([]Preprint
if len(result.ItemHits) < fetch {
break
}
skip += fetch
cursor += fetch
}
return out, total, nil
}

// Recent returns the most recent preprints (empty term search).
func (c *Client) Recent(ctx context.Context, limit int) ([]Preprint, int, error) {
return c.Search(ctx, "", limit)
return c.SearchFrom(ctx, "", 0, limit)
}

// RecentFrom returns recent preprints starting from the given skip offset.
func (c *Client) RecentFrom(ctx context.Context, skip, limit int) ([]Preprint, int, error) {
return c.SearchFrom(ctx, "", skip, limit)
}

// GetPreprint fetches a single preprint by its ChemRxiv ID.
Expand Down
48 changes: 48 additions & 0 deletions chemrxiv/chemrxiv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,51 @@ func TestRecent(t *testing.T) {
t.Error("Recent returned no preprints")
}
}

func TestSearchFromSkip(t *testing.T) {
// Verify that SearchFrom sends the skip value in the query and adjusts rank.
var capturedQuery string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var body map[string]string
_ = json.Unmarshal(b, &body)
capturedQuery = body["query"]
_, _ = w.Write([]byte(sampleSearchResponse))
}))
defer srv.Close()

c := newTestClient(srv)
preprints, _, err := c.SearchFrom(context.Background(), "oxygen", 10, 5)
if err != nil {
t.Fatal(err)
}
// Query should contain skip:10
if !strings.Contains(capturedQuery, "skip:10") {
t.Errorf("query does not contain skip:10; got: %s", capturedQuery)
}
// Ranks should start from skip+1 = 11
if len(preprints) > 0 && preprints[0].Rank != 11 {
t.Errorf("first rank = %d, want 11 (skip=10)", preprints[0].Rank)
}
}

func TestRateLimitedOn429(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTooManyRequests)
}))
defer srv.Close()

cfg := DefaultConfig()
cfg.BaseURL = srv.URL
cfg.Rate = 0
cfg.Retries = 2
c := NewClient(cfg)

_, _, err := c.Search(context.Background(), "test", 5)
if err == nil {
t.Fatal("expected error after 429 retries exhausted, got nil")
}
if !strings.Contains(err.Error(), "429") {
t.Errorf("error should mention 429, got: %v", err)
}
}
9 changes: 6 additions & 3 deletions cli/cmd_recent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import (
)

func (a *App) recentCmd() *cobra.Command {
return &cobra.Command{
var skip int
cmd := &cobra.Command{
Use: "recent",
Short: "List the most recent ChemRxiv preprints",
Long: "Fetch the most recently submitted ChemRxiv preprints. Default: up to 20 results.",
RunE: func(cmd *cobra.Command, _ []string) error {
n := a.effectiveLimit(20)
a.progressf("fetching %d recent preprints...", n)
preprints, _, err := a.client.Recent(cmd.Context(), n)
a.progressf("fetching %d recent preprints (skip %d)...", n, skip)
preprints, _, err := a.client.RecentFrom(cmd.Context(), skip, n)
if err != nil {
return mapFetchErr(err)
}
return a.renderOrEmpty(preprints, len(preprints))
},
}
cmd.Flags().IntVar(&skip, "skip", 0, "number of results to skip before returning")
return cmd
}
9 changes: 6 additions & 3 deletions cli/cmd_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
)

func (a *App) searchCmd() *cobra.Command {
return &cobra.Command{
var skip int
cmd := &cobra.Command{
Use: "search <query>",
Short: "Search ChemRxiv preprints by keyword",
Long: `Search ChemRxiv preprints by keyword using the public GraphQL API.
Expand All @@ -14,13 +15,15 @@ Paginates automatically until the requested limit is reached.`,
RunE: func(cmd *cobra.Command, args []string) error {
query := args[0]
n := a.effectiveLimit(20)
a.progressf("searching for %q (limit %d)...", query, n)
preprints, total, err := a.client.Search(cmd.Context(), query, n)
a.progressf("searching for %q (skip %d, limit %d)...", query, skip, n)
preprints, total, err := a.client.SearchFrom(cmd.Context(), query, skip, n)
if err != nil {
return mapFetchErr(err)
}
a.progressf("found %d total; showing %d", total, len(preprints))
return a.renderOrEmpty(preprints, len(preprints))
},
}
cmd.Flags().IntVar(&skip, "skip", 0, "number of results to skip before returning")
return cmd
}
Loading