Skip to content
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

Enhancement: clients can use REST url for all methods #219

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a5803bd
feat: added project v1 to rest conversion class
davidalexandru7013 Aug 9, 2024
5cee33b
feat: added method to check if a path is project v1
davidalexandru7013 Aug 9, 2024
39d9486
fix: updated delete project tests to mock rest route
davidalexandru7013 Aug 11, 2024
62e3c3b
chore: black formatting of client, managers and models files
davidalexandru7013 Aug 11, 2024
0ee912d
feat: moved get project by id from v1 to rest
davidalexandru7013 Aug 12, 2024
8fc8a36
fix: replace old route with new route for projects in tests
davidalexandru7013 Aug 12, 2024
cfe46fe
feat: patch method in client
davidalexandru7013 Aug 12, 2024
eeddafe
chore: removed debug from examples
davidalexandru7013 Aug 12, 2024
956394e
chore: removed ununsed class
davidalexandru7013 Aug 12, 2024
659827e
chore: replaced old api version with new one
davidalexandru7013 Aug 12, 2024
5c21f1d
feat: added patch update project test
davidalexandru7013 Aug 12, 2024
3b76531
feat: tests for patch update project
davidalexandru7013 Aug 12, 2024
f0a05e2
chore: added get project by id and tests back
davidalexandru7013 Aug 12, 2024
d6db244
chore: use rest boolean for clients and tests
davidalexandru7013 Aug 12, 2024
a04902a
chore: send query params
davidalexandru7013 Aug 12, 2024
2bc763f
feat: tests to avoid side-effects for api headers
davidalexandru7013 Aug 13, 2024
90f67bc
feat: clients can use rest path for delete and tests
davidalexandru7013 Aug 13, 2024
a823df4
chore: removed example file
davidalexandru7013 Aug 13, 2024
b849478
chore: rename test from new project to updated project
davidalexandru7013 Aug 13, 2024
657d4ec
chore: replace dict with Dict and extract latest version in a variable
davidalexandru7013 Aug 20, 2024
45d2ec8
chore: replace type from any to dict
davidalexandru7013 Aug 20, 2024
0998780
fix: added rest content type for post requests when calling rest route
davidalexandru7013 Aug 20, 2024
d4fe020
fix: added rest content type for put method and tests
davidalexandru7013 Aug 20, 2024
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
2 changes: 1 addition & 1 deletion examples/api-demo-1-list-projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ def parse_command_line_args():
print(" Issues Found:")
print(" High : %s" % proj.issueCountsBySeverity.high)
print(" Medium: %s" % proj.issueCountsBySeverity.medium)
print(" Low : %s" % proj.issueCountsBySeverity.low)
print(" Low : %s" % proj.issueCountsBySeverity.low)
128 changes: 115 additions & 13 deletions snyk/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import copy
import logging
import re
import urllib.parse
from typing import Any, List, Optional
from typing import Any, Dict, List, Optional, Pattern
from urllib.parse import parse_qs, urlparse

import requests
Expand Down Expand Up @@ -40,13 +42,19 @@ def __init__(
"Authorization": "token %s" % self.api_token,
"User-Agent": user_agent,
}
self.api_post_headers = self.api_headers
self.api_post_headers = dict(self.api_headers)
self.api_post_headers["Content-Type"] = "application/json"
self.api_patch_headers = dict(self.api_headers)
self.api_patch_headers["Content-Type"] = "application/vnd.api+json"
self.tries = tries
self.backoff = backoff
self.delay = delay
self.verify = verify
self.version = version
self.__uuid_pattern = (
r"[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
)
self.__latest_version = "2024-06-21"

# Ensure we don't have a trailing /
if self.api_url[-1] == "/":
Expand Down Expand Up @@ -82,14 +90,31 @@ def request(
raise SnykHTTPError(resp)
return resp

def post(self, path: str, body: Any, headers: dict = {}) -> requests.Response:
url = f"{self.api_url}/{path}"
def post(
self,
path: str,
body: Dict[str, Any],
headers: Dict = {},
params: Dict[str, Any] = {},
use_rest: bool = False,
) -> requests.Response:
url = f"{self.rest_api_url if use_rest else self.api_url}/{path}"
logger.debug(f"POST: {url}")

request_headers = {**self.api_post_headers, **headers}
if use_rest:
if "version" not in params:
params["version"] = self.version or self.__latest_version
request_headers["Content-Type"] = "application/vnd.api+json"

resp = retry_call(
self.request,
fargs=[requests.post, url],
fkwargs={"json": body, "headers": {**self.api_post_headers, **headers}},
fkwargs={
"json": body,
"headers": request_headers,
"params": params,
},
tries=self.tries,
delay=self.delay,
backoff=self.backoff,
Expand All @@ -103,14 +128,68 @@ def post(self, path: str, body: Any, headers: dict = {}) -> requests.Response:

return resp

def put(self, path: str, body: Any, headers: dict = {}) -> requests.Response:
url = "%s/%s" % (self.api_url, path)
def patch(
self,
path: str,
body: Dict[str, Any],
headers: Dict[str, str] = {},
params: Dict[str, Any] = {},
) -> requests.Response:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should keep consistency here with a rest parameter or not. I know it would be useless, and need to be set to true. It's just triggering my OCD having this lack of uniformity of methods 🙈

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation(https://snyk.docs.apiary.io/#reference) there is no endpoint which makes use of patch verb in V1.

url = f"{self.rest_api_url}/{path}"
logger.debug(f"PATCH: {url}")

if "version" not in params:
params["version"] = self.version or self.__latest_version

resp = retry_call(
self.request,
fargs=[requests.patch, url],
fkwargs={
"json": body,
"headers": {**self.api_patch_headers, **headers},
"params": params,
},
tries=self.tries,
delay=self.delay,
backoff=self.backoff,
logger=logger,
)

if not resp.ok:
logger.error(resp.text)
raise SnykHTTPError(resp)

return resp

def put(
self,
path: str,
body: Any,
headers: Dict = {},
params: Dict[str, Any] = {},
use_rest: bool = False,
) -> requests.Response:
url = "%s/%s" % (
self.rest_api_url if use_rest else self.api_url,
path,
)
logger.debug("PUT: %s" % url)

request_headers = {**self.api_post_headers, **headers}

if use_rest:
if "version" not in params:
params["version"] = self.version or self.__latest_version
request_headers["Content-Type"] = "application/vnd.api+json"

resp = retry_call(
self.request,
fargs=[requests.put, url],
fkwargs={"json": body, "headers": {**self.api_post_headers, **headers}},
fkwargs={
"json": body,
"headers": request_headers,
"params": params,
},
tries=self.tries,
delay=self.delay,
backoff=self.backoff,
Expand All @@ -125,7 +204,7 @@ def put(self, path: str, body: Any, headers: dict = {}) -> requests.Response:
def get(
self,
path: str,
params: dict = None,
params: Dict = None,
version: str = None,
exclude_version: bool = False,
exclude_params: bool = False,
Expand Down Expand Up @@ -196,14 +275,26 @@ def get(

return resp

def delete(self, path: str) -> requests.Response:
url = f"{self.api_url}/{path}"
def delete(self, path: str, use_rest: bool = False) -> requests.Response:
is_v1_project_path: bool = self.__is_v1_project_path(path)
if is_v1_project_path:
ids = re.findall(rf"{self.__uuid_pattern}", path)
path = f"orgs/{ids[0]}/projects/{ids[1]}"
url = f"{self.rest_api_url}/{path}"
use_rest = True
else:
url = f"{self.rest_api_url if use_rest else self.api_url}/{path}"

params = {}
if use_rest:
params["version"] = self.version or self.__latest_version

logger.debug(f"DELETE: {url}")

resp = retry_call(
self.request,
fargs=[requests.delete, url],
fkwargs={"headers": self.api_headers},
fkwargs={"headers": self.api_headers, "params": params},
tries=self.tries,
delay=self.delay,
backoff=self.backoff,
Expand All @@ -215,7 +306,7 @@ def delete(self, path: str) -> requests.Response:

return resp

def get_rest_pages(self, path: str, params: dict = {}) -> List:
def get_rest_pages(self, path: str, params: Dict = {}) -> List:
"""
Helper function to collect paginated responses from the rest API into a single
list.
Expand Down Expand Up @@ -293,3 +384,14 @@ def groups(self):
# https://snyk.docs.apiary.io/#reference/reporting-api/issues/get-list-of-issues
def issues(self):
raise SnykNotImplementedError # pragma: no cover

def __is_v1_project_path(self, path: str) -> bool:
v1_paths: List[Pattern[str]] = [
re.compile(f"^org/{self.__uuid_pattern}/project/{self.__uuid_pattern}$")
]

for v1_path in v1_paths:
if re.match(v1_path, path):
return True

return False
1 change: 0 additions & 1 deletion snyk/managers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import abc
import json
from typing import Any, Dict, List

from deprecation import deprecated # type: ignore
Expand Down
3 changes: 1 addition & 2 deletions snyk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from dataclasses import InitVar, dataclass, field
from typing import Any, Dict, List, Optional, Union

import requests
from deprecation import deprecated # type: ignore
from mashumaro.mixins.json import DataClassJSONMixin # type: ignore

Expand Down Expand Up @@ -245,7 +244,7 @@ def import_project(self, url, files: Optional[List[str]] = None) -> bool:
# https://snyk.docs.apiary.io/#reference/users/user-organisation-notification-settings/modify-org-notification-settings
# https://snyk.docs.apiary.io/#reference/users/user-organisation-notification-settings/get-org-notification-settings
def notification_settings(self):
raise SnykNotImplemented # pragma: no cover
raise SnykNotImplementedError # pragma: no cover

# https://snyk.docs.apiary.io/#reference/organisations/the-snyk-organisation-for-a-request/invite-users
def invite(self, email: str, admin: bool = False) -> bool:
Expand Down
Loading