Repository: safedep/dry
Pull request #30: Period wise download stats for registry adapters
Author: login: OmkarPh
Created: 2025-05-15T15:33:34Z | Merged: 2025-05-18T12:46:15Z

Fulfills requirements for https://github.com/safedep/malysis/pull/137


Diff:
diff --git a/packageregistry/github.go b/packageregistry/github.go
index c642a43..e21f1bc 100644
--- a/packageregistry/github.go
+++ b/packageregistry/github.go
@@ -14,6 +14,9 @@ type githubPackageRegistryAdapter struct {
 	gitHubClient *adapters.GithubClient
 }
 
+// Verify that githubPackageRegistryAdapter implements the Client interface
+var _ Client = (*githubPackageRegistryAdapter)(nil)
+
 type githubPackageRegistryPublisherDiscovery struct {
 	gitHubClient *adapters.GithubClient
 }
@@ -136,6 +139,10 @@ func (ga *githubPackageRegistryPackageDiscovery) GetPackage(packageName string)
 	return githubRegistryCreatePackageWrapper(repository, latestVersion, pkgVersions), nil
 }
 
+func (ga *githubPackageRegistryPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	return DownloadStats{}, fmt.Errorf("download stats are not supported for GitHub adapter")
+}
+
 // getGitHubRepositoryLatestVersion returns the latest version of the repository
 // If there is no release, it returns the default branch
 // We only return RateLimitError if the rate limit is exceeded
diff --git a/packageregistry/npm.go b/packageregistry/npm.go
index ccd1aeb..b0f4190 100644
--- a/packageregistry/npm.go
+++ b/packageregistry/npm.go
@@ -122,6 +122,31 @@ func (np *npmPackageDiscovery) GetPackageDependencies(packageName string, packag
 	}, nil
 }
 
+func (np *npmPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	downloadPeriods := []npmDownloadPeriod{
+		npmDownloadsPeriodLastDay,
+		npmDownloadsPeriodLastWeek,
+		npmDownloadsPeriodLastMonth,
+		npmDownloadsPeriodLastYear,
+	}
+	periodWiseDownloads := make(map[npmDownloadPeriod]uint64)
+
+	var err error
+	for _, period := range downloadPeriods {
+		periodWiseDownloads[period], err = npmGetPackageDownloadsForPeriod(packageName, period)
+		if err != nil {
+			return DownloadStats{}, err
+		}
+	}
+
+	return DownloadStats{
+		Daily:   periodWiseDownloads[npmDownloadsPeriodLastDay],
+		Weekly:  periodWiseDownloads[npmDownloadsPeriodLastWeek],
+		Monthly: periodWiseDownloads[npmDownloadsPeriodLastMonth],
+		Total:   periodWiseDownloads[npmDownloadsPeriodLastYear],
+	}, nil
+}
+
 func npmGetPackageVersionDetails(packageName string, packageVersion string) (*npmPackageVersionInfo, error) {
 	url := npmAPIEndpointPackageWithVersionURL(packageName, packageVersion)
 
@@ -187,7 +212,7 @@ func npmGetPackageDetails(packageName string) (*Package, error) {
 		})
 	}
 
-	downloads, err := npmGetPackageDownloads(packageName)
+	downloads, err := npmGetPackageDownloadsForPeriod(packageName, npmDownloadsPeriodLastYear)
 	if err != nil {
 		return nil, err
 	}
@@ -217,8 +242,17 @@ func npmGetPackageDetails(packageName string) (*Package, error) {
 	return &pkg, nil
 }
 
-func npmGetPackageDownloads(packageName string) (uint64, error) {
-	url := npmAPIEndpointPackageDownloadsURL(packageName)
+type npmDownloadPeriod string
+
+const (
+	npmDownloadsPeriodLastDay   npmDownloadPeriod = "last-day"
+	npmDownloadsPeriodLastWeek  npmDownloadPeriod = "last-week"
+	npmDownloadsPeriodLastMonth npmDownloadPeriod = "last-month"
+	npmDownloadsPeriodLastYear  npmDownloadPeriod = "last-year"
+)
+
+func npmGetPackageDownloadsForPeriod(packageName string, period npmDownloadPeriod) (uint64, error) {
+	url := npmAPIEndpointPackageDownloadsURL(packageName, period)
 
 	res, err := http.Get(url)
 	if err != nil {
@@ -226,14 +260,14 @@ func npmGetPackageDownloads(packageName string) (uint64, error) {
 	}
 	defer res.Body.Close()
 
-	if res.StatusCode != http.StatusOK {
-		return 0, ErrFailedToFetchPackage
-	}
-
 	if res.StatusCode == http.StatusNotFound {
 		return 0, ErrPackageNotFound
 	}
 
+	if res.StatusCode != http.StatusOK {
+		return 0, ErrFailedToFetchPackage
+	}
+
 	var downloadObject npmDownloadObject
 	err = json.NewDecoder(res.Body).Decode(&downloadObject)
 	if err != nil {
diff --git a/packageregistry/npm_endpoints.go b/packageregistry/npm_endpoints.go
index aa378fa..61c54c1 100644
--- a/packageregistry/npm_endpoints.go
+++ b/packageregistry/npm_endpoints.go
@@ -17,6 +17,8 @@ func npmAPIEndpointPackageSearchWithAuthorURL(author string) string {
 	return fmt.Sprintf("https://registry.npmjs.org/-/v1/search?text=author:%s", author)
 }
 
-func npmAPIEndpointPackageDownloadsURL(packageName string) string {
-	return fmt.Sprintf("https://api.npmjs.org/downloads/point/last-year/%s", packageName)
+// Gets the download count for a package in the specified period
+// periodPoint can be "last-day", "last-week", "last-month", "last-year"
+func npmAPIEndpointPackageDownloadsURL(packageName string, periodPoint npmDownloadPeriod) string {
+	return fmt.Sprintf("https://api.npmjs.org/downloads/point/%s/%s", periodPoint, packageName)
 }
diff --git a/packageregistry/npm_test.go b/packageregistry/npm_test.go
index 8422d86..0406fa3 100644
--- a/packageregistry/npm_test.go
+++ b/packageregistry/npm_test.go
@@ -144,6 +144,58 @@ func TestNpmGetPackagesByPublisher(t *testing.T) {
 	}
 }
 
+func TestNpmPackageDiscoveryDownloadStats(t *testing.T) {
+	cases := []struct {
+		pkgName                     string
+		expectedError               error
+		expectedMinDailyDownloads   uint64
+		expectedMinWeeklyDownloads  uint64
+		expectedMinMonthlyDownloads uint64
+		expectedMinTotalDownloads   uint64
+	}{
+		{
+			pkgName:                     "express",
+			expectedError:               nil,
+			expectedMinDailyDownloads:   1000_000,
+			expectedMinWeeklyDownloads:  7000_000,
+			expectedMinMonthlyDownloads: 30_000_000,
+			expectedMinTotalDownloads:   1_000_000_000, // express downloads on last year, we will check >= this
+		},
+		{
+			pkgName:                     "@kunalsin9h/load-gql",
+			expectedError:               nil,
+			expectedMinDailyDownloads:   0,
+			expectedMinWeeklyDownloads:  0,
+			expectedMinMonthlyDownloads: 0,
+			expectedMinTotalDownloads:   50,
+		},
+		{
+			pkgName:       "random-package-name-that-does-not-exist-1246890",
+			expectedError: ErrPackageNotFound,
+		},
+	}
+	packageDiscovery := npmPackageDiscovery{}
+	for _, test := range cases {
+		t.Run(test.pkgName, func(t *testing.T) {
+			t.Parallel()
+
+			downloadStats, err := packageDiscovery.GetPackageDownloadStats(test.pkgName)
+			if test.expectedError != nil {
+				assert.ErrorIs(t, err, test.expectedError)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, downloadStats)
+
+				// Downloads data
+				assert.GreaterOrEqual(t, downloadStats.Daily, test.expectedMinDailyDownloads)
+				assert.GreaterOrEqual(t, downloadStats.Weekly, test.expectedMinWeeklyDownloads)
+				assert.GreaterOrEqual(t, downloadStats.Monthly, test.expectedMinMonthlyDownloads)
+				assert.GreaterOrEqual(t, downloadStats.Total, test.expectedMinTotalDownloads)
+			}
+		})
+	}
+}
+
 func TestNpmGetPackage(t *testing.T) {
 	cases := []struct {
 		pkgName string
@@ -166,7 +218,7 @@ func TestNpmGetPackage(t *testing.T) {
 		{
 			pkgName:              "@kunalsin9h/load-gql",
 			expectedError:        nil,
-			expectedMinDownloads: 90,
+			expectedMinDownloads: 50,
 			expectedRepoURL:      "https://github.com/kunalsin9h/load-gql",
 			expectedPublishers: Publisher{
 				Name:  "Kunal Singh",
diff --git a/packageregistry/pypi.go b/packageregistry/pypi.go
index 4460319..9413952 100644
--- a/packageregistry/pypi.go
+++ b/packageregistry/pypi.go
@@ -9,6 +9,10 @@ import (
 )
 
 type pypiAdapter struct{}
+
+// Verify that pypiAdapter implements the Client interface
+var _ Client = (*pypiAdapter)(nil)
+
 type pypiPublisherDiscovery struct{}
 type pypiPackageDiscovery struct{}
 
@@ -136,8 +140,8 @@ func (np *pypiPackageDiscovery) GetPackage(packageName string) (*Package, error)
 		Maintainers:         maintainers,
 		LatestVersion:       pypipkg.Info.LatestVersion,
 		Versions:            pkgVersions,
-		// Do offical way to get downloads
-		// Thought we can use pypi.tech
+		// No official way to get downloads
+		// Though, we can use pypi.tech
 		// https://api.pepy.tech/api/v2/projects/requests
 		// But it require API key
 		Downloads: OptionalInt{
@@ -148,3 +152,7 @@ func (np *pypiPackageDiscovery) GetPackage(packageName string) (*Package, error)
 
 	return &pkg, nil
 }
+
+func (np *pypiPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	return DownloadStats{}, fmt.Errorf("download stats is not supported for PyPI adapter")
+}
diff --git a/packageregistry/pypi_test.go b/packageregistry/pypi_test.go
index 3dd5c8f..eb015b1 100644
--- a/packageregistry/pypi_test.go
+++ b/packageregistry/pypi_test.go
@@ -1,10 +1,11 @@
 package packageregistry
 
 import (
-	"github.com/safedep/dry/semver"
 	"reflect"
 	"testing"
 
+	"github.com/safedep/dry/semver"
+
 	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
 	"github.com/stretchr/testify/assert"
 )
diff --git a/packageregistry/registry.go b/packageregistry/registry.go
index a45074b..6188960 100644
--- a/packageregistry/registry.go
+++ b/packageregistry/registry.go
@@ -51,6 +51,13 @@ type PackageVersionInfo struct {
 	Version string `json:"version"`
 }
 
+type DownloadStats struct {
+	Daily   uint64 `json:"daily"`
+	Weekly  uint64 `json:"weekly"`
+	Monthly uint64 `json:"monthly"`
+	Total   uint64 `json:"total"`
+}
+
 // Package represents a package in a package registry.
 // Example: `requests` in PyPI, `rails` in RubyGems.
 type Package struct {
@@ -94,6 +101,9 @@ type PackageDiscovery interface {
 
 	// GetPackageDependencies returns the dependencies for the given package version.
 	GetPackageDependencies(packageName string, packageVersion string) (*PackageDependencyList, error)
+
+	// GetPackageDownloadStats returns the download stats for the given package.
+	GetPackageDownloadStats(packageName string) (DownloadStats, error)
 }
 
 // Contract for implementing publisher discovery for a package registry.
diff --git a/packageregistry/ruby.go b/packageregistry/ruby.go
index ee8858a..0b48139 100644
--- a/packageregistry/ruby.go
+++ b/packageregistry/ruby.go
@@ -9,6 +9,10 @@ import (
 )
 
 type rubyAdapter struct{}
+
+// Verify that rubyAdapter implements the Client interface
+var _ Client = (*rubyAdapter)(nil)
+
 type rubyPublisherDiscovery struct{}
 type rubyPackageDiscovery struct{}
 
@@ -136,6 +140,10 @@ func (np *rubyPackageDiscovery) GetPackage(packageName string) (*Package, error)
 	return convertGemObjectToPackage(gemObject)
 }
 
+func (np *rubyPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	return DownloadStats{}, fmt.Errorf("download stats are not supported for Ruby adapter")
+}
+
 func convertGemObjectToPackage(gemObject gemObject) (*Package, error) {
 	pkgVersions, err := getPackageVersions(gemObject.Name)
 	if err != nil {
diff --git a/packageregistry/ruby_test.go b/packageregistry/ruby_test.go
index d7eb0aa..712a28f 100644
--- a/packageregistry/ruby_test.go
+++ b/packageregistry/ruby_test.go
@@ -1,10 +1,11 @@
 package packageregistry
 
 import (
-	"github.com/safedep/dry/semver"
 	"reflect"
 	"testing"
 
+	"github.com/safedep/dry/semver"
+
 	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
 	"github.com/stretchr/testify/assert"
 )
@@ -137,9 +138,8 @@ func TestRubyGetPackage(t *testing.T) {
 		assert               func(t *testing.T, pkg *Package)
 	}{
 		{
-			name:    "Correct ruby package",
-			pkgName: "rails",
-
+			name:                 "Correct ruby package",
+			pkgName:              "rails",
 			expectedError:        nil,
 			expectedPackageName:  "rails",
 			expectedDescription:  true,
@@ -182,6 +182,7 @@ func TestRubyGetPackage(t *testing.T) {
 					assert.GreaterOrEqual(t, len(pkg.Description), 1) // Description is not empty
 				}
 				assert.Equal(t, pkg.SourceRepositoryUrl, test.expectedRepoURL)
+				assert.True(t, pkg.Downloads.Valid)
 				assert.GreaterOrEqual(t, pkg.Downloads.Value, test.expectedMinDownloads)
 				assert.Equal(t, pkg.Author.Name, test.expectedAuthorName)
 				assert.GreaterOrEqual(t, len(pkg.Versions), test.expectedMinVersions)


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the registry adapters to support period wise download statistics instead of a single aggregate value.  
- Removes the OptionalInt type and its JSON methods.  
- Introduces a new DownloadStats struct with daily, weekly, monthly, and total counts.  
- Updates tests and adapter implementations (Ruby, PyPI, NPM, GitHub) to handle the new download stats structure.

### Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                           | Description                                                                 |
| ------------------------------ | --------------------------------------------------------------------------- |
| packageregistry/types.go       | Removed the OptionalInt type and its custom JSON marshaling/unmarshaling.     |
| packageregistry/ruby_test.go   | Updated assertions to verify the new DownloadStats fields for Ruby packages.  |
| packageregistry/ruby.go        | Changed Downloads field to use DownloadStats and added client interface check.|
| packageregistry/registry.go    | Introduced the DownloadStats struct and updated the Package definition.     |
| packageregistry/pypi_test.go   | Adjusted import order and minor changes to reflect the API update.            |
| packageregistry/pypi.go        | Switched from OptionalInt to DownloadStats for downloads and updated comments.|
| packageregistry/npm_test.go    | Updated test expectations with new variables for period-specific downloads.   |
| packageregistry/npm_endpoints.go| Modified API endpoint to accept a period parameter for downloads.          |
| packageregistry/npm.go         | Implemented download aggregation logic for multiple periods.                |
| packageregistry/github.go      | Added verification to ensure the GitHub adapter implements the Client interface.|
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:34:25Z
user: login: abhisek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T15:43:38Z
user: login: OmkarPh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:44:46Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces period-wise download stats functionality for various registry adapters to fulfill the requirements of #137. Key changes include:
- Adding a new DownloadStats structure to the registry package and its implementations across Ruby, PyPI, npm, and GitHub.
- Updating the npm endpoints and download stats retrieval logic to support multiple download periods.
- Enhancing test coverage by adding tests for download stats in the npm package discovery.

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                                    |
| ----------------------------------- | -------------------------------------------------------------- |
| packageregistry/ruby_test.go        | Added assertion for Downloads validity in Ruby tests.         |
| packageregistry/ruby.go             | Added stub implementation for GetPackageDownloadStats.         |
| packageregistry/registry.go         | Introduced the DownloadStats structure and related API update. |
| packageregistry/pypi_test.go        | Adjusted import ordering; no functionality changes.            |
| packageregistry/pypi.go             | Added stub implementation for GetPackageDownloadStats.         |
| packageregistry/npm_test.go         | Added tests for package download stat retrieval; adjusted thresholds. |
| packageregistry/npm_endpoints.go    | Updated npm download endpoint to accept period parameters.     |
| packageregistry/npm.go              | Revised npm adapter to retrieve download stats per period.     |
| packageregistry/github.go           | Added stub implementation for GetPackageDownloadStats.         |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:00:30Z
user: login: OmkarPh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:00:30Z
user: login: abhisek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T08:20:53Z
user: login: abhisek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-18T08:18:17Z

Review comments:
diff_hunk: @@ -17,6 +17,8 @@ func npmAPIEndpointPackageSearchWithAuthorURL(author string) string {
 	return fmt.Sprintf("https://registry.npmjs.org/-/v1/search?text=author:%s", author)
 }
 
-func npmAPIEndpointPackageDownloadsURL(packageName string) string {
-	return fmt.Sprintf("https://api.npmjs.org/downloads/point/last-year/%s", packageName)
+// Gets the download count for a package in the specified period
+// periodPoint can be "last-day", "last-week", "last-month", "last-year"
+func npmAPIEndpointPackageDownloadsURL(packageName string, periodPoint string) string {
path: packageregistry/npm_endpoints.go
user: login: Copilot
body: Consider validating or documenting the accepted values for 'periodPoint' (e.g., 'last-day', 'last-week', 'last-month', 'last-year') to ensure consistency and prevent misuse.
created_at: 2025-05-15T15:34:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,14 +140,11 @@ func (np *pypiPackageDiscovery) GetPackage(packageName string) (*Package, error)
 		Maintainers:         maintainers,
 		LatestVersion:       pypipkg.Info.LatestVersion,
 		Versions:            pkgVersions,
-		// Do offical way to get downloads
-		// Thought we can use pypi.tech
+		// No offical way to get downloads
path: packageregistry/pypi.go
user: login: Copilot
body: Correct the spelling of 'offical' to 'official' in the comment.
```suggestion
		// No official way to get downloads
```
created_at: 2025-05-15T15:34:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,3 +242,22 @@ func npmGetPackageDownloads(packageName string) (uint64, error) {
 
 	return downloadObject.Downloads, nil
 }
+
+func npmGetPackageDownloads(packageName string) (DownloadStats, error) {
+	periods := []string{"last-day", "last-week", "last-month", "last-year"}
path: packageregistry/npm.go
user: login: abhisek
body: Is there no other way? We are introducing 4 API calls per `GetPackage(..)` API call. It will significantly slow down and reduce the reliability factor (due to multiple external system dependency) of this API.
created_at: 2025-05-15T15:43:36Z
author_association: MEMBER
diff_hunk: @@ -242,3 +242,22 @@ func npmGetPackageDownloads(packageName string) (uint64, error) {
 
 	return downloadObject.Downloads, nil
 }
+
+func npmGetPackageDownloads(packageName string) (DownloadStats, error) {
+	periods := []string{"last-day", "last-week", "last-month", "last-year"}
path: packageregistry/npm.go
user: login: OmkarPh
body: unfortunately no, npm provides per-period API call only
created_at: 2025-05-15T15:44:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,6 +145,58 @@ func TestNpmGetPackagesByPublisher(t *testing.T) {
 	}
 }
 
+func TestNpmPackageDiscoveryDownloadStats(t *testing.T) {
+	cases := []struct {
+		pkgName                     string
+		expectedError               error
+		expectedMinDailyDownloads   uint64
+		expectedMinWeeklyDownloads  uint64
+		expectedMinMonthlyDownloads uint64
+		expectedMinTotalDownloads   uint64
+	}{
+		{
+			pkgName:                     "express",
+			expectedError:               nil,
+			expectedMinDailyDownloads:   5000000,
+			expectedMinWeeklyDownloads:  30000000,
+			expectedMinMonthlyDownloads: 100000000,
+			expectedMinTotalDownloads:   1658725727, // express downoads on last year, we will check >= this
path: packageregistry/npm_test.go
user: login: Copilot
body: There is a spelling mistake in the comment: 'downoads' should be 'downloads'.
```suggestion
			expectedMinTotalDownloads:   1658725727, // express downloads on last year, we will check >= this
```
created_at: 2025-05-16T07:00:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,3 +242,22 @@ func npmGetPackageDownloads(packageName string) (uint64, error) {
 
 	return downloadObject.Downloads, nil
 }
+
+func npmGetPackageDownloads(packageName string) (DownloadStats, error) {
+	periods := []string{"last-day", "last-week", "last-month", "last-year"}
path: packageregistry/npm.go
user: login: OmkarPh
body: moving getdownloadstats to packagediscovery
created_at: 2025-05-16T07:00:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,6 +139,10 @@ func (ga *githubPackageRegistryPackageDiscovery) GetPackage(packageName string)
 	return githubRegistryCreatePackageWrapper(repository, latestVersion, pkgVersions), nil
 }
 
+func (ga *githubPackageRegistryPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	return DownloadStats{}, nil
path: packageregistry/github.go
user: login: abhisek
body: Return error please ie. download stats is not supported, otherwise it will cause bugs in client code assuming it to be `0` downloads
created_at: 2025-05-16T08:19:21Z
author_association: MEMBER
diff_hunk: @@ -148,3 +152,7 @@ func (np *pypiPackageDiscovery) GetPackage(packageName string) (*Package, error)
 
 	return &pkg, nil
 }
+
+func (np *pypiPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	return DownloadStats{}, nil
path: packageregistry/pypi.go
user: login: abhisek
body: Return error please ie. download stats is not supported, otherwise it will cause bugs in client code assuming it to be `0` downloads
created_at: 2025-05-16T08:20:29Z
author_association: MEMBER
diff_hunk: @@ -136,6 +140,10 @@ func (np *rubyPackageDiscovery) GetPackage(packageName string) (*Package, error)
 	return convertGemObjectToPackage(gemObject)
 }
 
+func (np *rubyPackageDiscovery) GetPackageDownloadStats(packageName string) (DownloadStats, error) {
+	return DownloadStats{}, nil
path: packageregistry/ruby.go
user: login: abhisek
body: Return error please ie. download stats is not supported, otherwise it will cause bugs in client code assuming it to be `0` downloads
created_at: 2025-05-16T08:20:50Z
author_association: MEMBER

PR comments:
user: login: OmkarPh
created_at: 2025-05-18T08:13:20Z
body: Updated
author_association: CONTRIBUTOR
user: login: abhisek
created_at: 2025-05-18T08:19:23Z
body: @OmkarPh LGTM. Approved. 
author_association: MEMBER