From 88716eb690de4213e91f968716a0121031224bd6 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 16 Oct 2024 16:25:23 +0530 Subject: [PATCH 01/44] feat: add feeds api with examples and wrapper files --- examples/auth/get_workspace_id.py | 26 ++ examples/feeds/create_feed_apis.py | 39 ++ examples/feeds/upload_package_apis.py | 48 +++ helpers/feeds_wrapper.py | 163 ++++++++ nisystemlink/clients/auth/__init__.py | 3 + nisystemlink/clients/auth/_auth_client.py | 35 ++ nisystemlink/clients/auth/models/__init__.py | 3 + .../clients/auth/models/_auth_info.py | 23 ++ .../clients/auth/models/_auth_models.py | 369 ++++++++++++++++++ nisystemlink/clients/feeds/feeds_client.py | 81 ++++ nisystemlink/clients/feeds/models/__init__.py | 10 + .../clients/feeds/models/_feeds_models.py | 87 +++++ 12 files changed, 887 insertions(+) create mode 100644 examples/auth/get_workspace_id.py create mode 100644 examples/feeds/create_feed_apis.py create mode 100644 examples/feeds/upload_package_apis.py create mode 100644 helpers/feeds_wrapper.py create mode 100644 nisystemlink/clients/auth/__init__.py create mode 100644 nisystemlink/clients/auth/_auth_client.py create mode 100644 nisystemlink/clients/auth/models/__init__.py create mode 100644 nisystemlink/clients/auth/models/_auth_info.py create mode 100644 nisystemlink/clients/auth/models/_auth_models.py create mode 100644 nisystemlink/clients/feeds/feeds_client.py create mode 100644 nisystemlink/clients/feeds/models/__init__.py create mode 100644 nisystemlink/clients/feeds/models/_feeds_models.py diff --git a/examples/auth/get_workspace_id.py b/examples/auth/get_workspace_id.py new file mode 100644 index 00000000..8c3177af --- /dev/null +++ b/examples/auth/get_workspace_id.py @@ -0,0 +1,26 @@ +"""Functionality of getting authentication information.""" + +from nisystemlink.clients.auth import AuthClient +from nisystemlink.clients.core import ApiException, HttpConfiguration + + +server_url = None # SystemLink API URL +server_api_key = None # SystemLink API key +workspace_name = None # Systemlink workspace name + + +auth_client = AuthClient(HttpConfiguration(server_uri=server_url, api_key=server_api_key)) + +try: + caller_info = auth_client.authenticate() + workspaces_info = caller_info.workspaces + + for workspace_info in workspaces_info: + if workspace_info.name == workspace_name: + print(workspace_info.id) + +except ApiException as exp: + print(exp) + +except Exception as exp: + print(exp) diff --git a/examples/feeds/create_feed_apis.py b/examples/feeds/create_feed_apis.py new file mode 100644 index 00000000..19345a29 --- /dev/null +++ b/examples/feeds/create_feed_apis.py @@ -0,0 +1,39 @@ +"""Functionality of creating feeds APIs.""" + +from nisystemlink.clients.core import ApiException, HttpConfiguration +from nisystemlink.clients.feeds.feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds.models import ( + CreateFeedRequest, + Platform, +) + + +# Constant +FEED_NAME = "EXAMPLE FEED" +FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" +PLATFORM = Platform.WINDOWS.value + +server_url = None # SystemLink API URL +server_api_key = None # SystemLink API key +workspace_id = None # Systemlink workspace id + +# Please provide the valid API key and API URL for client intialization. +client = SystemLinkFeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) + +# Creating Feeds. +try: + feed_request = CreateFeedRequest( + name=FEED_NAME, + description=FEED_DESCRIPTION, + platform=PLATFORM, + workspace=workspace_id, + ) + example_feed = client.create_feed(feed=feed_request).name + + print("Feeds created Successfully.") + print(f"Created feed name: {example_feed}") + +except ApiException as exp: + print(exp) +except Exception as exp: + print(exp) diff --git a/examples/feeds/upload_package_apis.py b/examples/feeds/upload_package_apis.py new file mode 100644 index 00000000..319de5a4 --- /dev/null +++ b/examples/feeds/upload_package_apis.py @@ -0,0 +1,48 @@ +"""Functionality of uploading & querying feeds APIs.""" + +from nisystemlink.clients.core import ApiException, HttpConfiguration +from nisystemlink.clients.feeds.feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds.models import Platform + + +# Constant +FEED_NAME = "EXAMPLE FEED" +FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" +PLATFORM = Platform.WINDOWS.value +PACKAGE_NAME = "" +PACKAGE_PATH = "" + +server_url = None # SystemLink API URL +server_api_key = None # SystemLink API key +workspace_id = None # Systemlink workspace id + +# Please provide the valid API key and API URL for client intialization. +client = SystemLinkFeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) + +# To upload a package to feed. +try: + # To query available feeds. + query_feeds = client.query_feeds( + platform=PLATFORM, + workspace=workspace_id, + ) + existing_feeds = {} + feed_id = "" + for feed in query_feeds.feeds: + if feed.name == FEED_NAME: + feed_id = feed.id + break + + upload_package = client.upload_package( + feed_id=feed_id, + overwrite=True, + package=(PACKAGE_NAME, open(PACKAGE_PATH, "rb"), "multipart/form-data"), + ) + print("Package uploaded sucessfully.") + print(f"Upload package: {upload_package.metadata.package_name}") + +except ApiException as exp: + print(exp) + +except Exception as exp: + print(exp) diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py new file mode 100644 index 00000000..a432b944 --- /dev/null +++ b/helpers/feeds_wrapper.py @@ -0,0 +1,163 @@ +"""Helper functions for using systemlink feeds APIs.""" + +import os +from typing import List, Union + +from nisystemlink.clients.auth import AuthClient +from nisystemlink.clients.core import ApiException, HttpConfiguration +from nisystemlink.clients.feeds.feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds.models import ( + CreateFeedRequest, + CreateOrUpdateFeedResponse, + Platform, +) +NIPKG = ".nipkg" + + +def __get_feed_platform(package_path: str) -> str: + _, pkg_ext = os.path.splitext(package_path) + + if pkg_ext == NIPKG: + return Platform.WINDOWS.value + + return Platform.NI_LINUX_RT.value + + +def __get_workspace_id( + workspace_name: str, server_api_key: str, server_url: str, +) -> Union[str, None]: + auth_client = AuthClient(HttpConfiguration(server_uri=server_url, api_key=server_api_key)) + caller_info = auth_client.authenticate() + workspaces_info = caller_info.workspaces + + for workspace_info in workspaces_info: + if workspace_info.name == workspace_name: + return workspace_info.id + + +def create_feed( + feed_name: str, + platform: str, + workspace_id: str, + client: SystemLinkFeedsClient +) -> CreateOrUpdateFeedResponse: + """Create new feed in systemlink. + + Args: + feed_name (str): Name of the feed. + platform (str): Name of the platform. + workspace_id (str): Workspace ID. + client (SystemLinkFeedsClient): Systemlink feeds Client. + + Returns: + CreateOrUpdateFeedResponse: Create feed response. + """ + create_feed_request = CreateFeedRequest( + name=feed_name, + workspace=workspace_id, + platform=platform, + ) + create_feed_response = client.create_feed(create_feed_request) + return create_feed_response + + +def upload_single_package( + package_path: str, + workspace_name: str, + server_api_key: str, + server_url: str, + feed_name: str, + overwrite: bool, +) -> str: + """Upload package to `SystemLink` feeds. + + Args: + package_path (str): Path of the package file. + workspace_name (str): Workspace name. + server_api_key (str): Systemlink API Key. + server_url (str): Systemlink API URL. + feed_name (str): Name of the feed. + overwrite (bool): To overwrite the package if exists. Defaults to false. + + Returns: + str: Upload package response. + """ + try: + platform = __get_feed_platform(package_path) + client = SystemLinkFeedsClient( + HttpConfiguration(api_key=server_api_key, server_uri=server_url) + ) + workspace_id = __get_workspace_id( + workspace_name=workspace_name, + server_api_key=server_api_key, + server_url=server_url, + ) + + query_feeds = client.query_feeds( + platform=platform, + workspace=workspace_id, + ) + existing_feeds = {} + for feed in query_feeds.feeds: + existing_feeds[feed.name] = feed.id + + if feed_name not in existing_feeds: + feed_id = create_feed( + feed_name=feed_name, + workspace_id=workspace_id, + client=client, + platform=platform, + ) + else: + feed_id = existing_feeds[feed_name] + + package_name = os.path.basename(package_path) + response = client.upload_package( + feed_id=feed_id, + package=(package_name, open(package_path, "rb"), "multipart/form-data"), + overwrite=overwrite, + ) + + return response.file_name + + except (ApiException, Exception) as exp: + return exp.message + + +def upload_multiple_packages( + package_paths: List[str], + workspace_name: str, + server_api_key: str, + server_url: str, + feed_name: str, + overwrite: bool, +) -> str: + """Upload multiple packages to systemlink feeds. + + Args: + package_paths (List[str]): List of package file paths. + workspace_name (str): Workspace name. + server_api_key (str): Systemlink API Key. + server_url (str): Systemlink API URL. + feed_name (str): Name of the feed. + overwrite (bool): To overwrite the package if exists. + + Returns: + str: Upload package responses. + """ + failed_packages = [] + for package_path in package_paths: + try: + upload_single_package( + package_path=package_path, + server_api_key=server_api_key, + server_url=server_url, + workspace_name=workspace_name, + overwrite=overwrite, + feed_name=feed_name, + ) + + except (ApiException, Exception) as exp: + failed_packages.append(exp.message) + + return failed_packages diff --git a/nisystemlink/clients/auth/__init__.py b/nisystemlink/clients/auth/__init__.py new file mode 100644 index 00000000..5d80edbd --- /dev/null +++ b/nisystemlink/clients/auth/__init__.py @@ -0,0 +1,3 @@ +from ._auth_client import AuthClient + +# flake8: noqa diff --git a/nisystemlink/clients/auth/_auth_client.py b/nisystemlink/clients/auth/_auth_client.py new file mode 100644 index 00000000..a0d8b4f3 --- /dev/null +++ b/nisystemlink/clients/auth/_auth_client.py @@ -0,0 +1,35 @@ +"""Implementation of AuthClient.""" + +from typing import Optional + +from nisystemlink.clients import core +from nisystemlink.clients.core._uplink._base_client import BaseClient +from nisystemlink.clients.core._uplink._methods import get + +from . import models + + +class AuthClient(BaseClient): + """Class contains a set of methods to access the APIs of SystemLink Auth Client.""" + + def __init__(self, configuration: Optional[core.HttpConfiguration] = None): + """Initialize an instance. + + Args: + configuration: Defines the web server to connect to and information about + how to connect. If not provided, an instance of + :class:`JupyterHttpConfiguration ` # noqa: W505 + is used. + + Raises: + ApiException: if unable to communicate with the Auth Service. + """ + if configuration is None: + configuration = core.JupyterHttpConfiguration() + + super().__init__(configuration, base_path="/niauth/v1/") + + @get("auth") + def authenticate(self) -> models.AuthInfo: + """Authenticates the given x-ni-api-key and returns information about the caller.""" + ... diff --git a/nisystemlink/clients/auth/models/__init__.py b/nisystemlink/clients/auth/models/__init__.py new file mode 100644 index 00000000..e57ed92d --- /dev/null +++ b/nisystemlink/clients/auth/models/__init__.py @@ -0,0 +1,3 @@ +from ._auth_info import AuthInfo + +# flake8: noqa diff --git a/nisystemlink/clients/auth/models/_auth_info.py b/nisystemlink/clients/auth/models/_auth_info.py new file mode 100644 index 00000000..f0013011 --- /dev/null +++ b/nisystemlink/clients/auth/models/_auth_info.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel +from pydantic import Field + +from ._auth_models import AuthPolicy, Org, User, Workspace + + +class AuthInfo(JsonModel): + """Information about the authenticated caller.""" + + user: Optional[User] + """Details of authenticated caller""" + org: Optional[Org] + """Organization of authenticated caller""" + workspaces: Optional[List[Workspace]] + """List of workspaces the authenticated caller has access""" + policies: Optional[List[AuthPolicy]] + """List of policies for the authenticated caller""" + properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) + """A map of key value properties""" diff --git a/nisystemlink/clients/auth/models/_auth_models.py b/nisystemlink/clients/auth/models/_auth_models.py new file mode 100644 index 00000000..6f86aa43 --- /dev/null +++ b/nisystemlink/clients/auth/models/_auth_models.py @@ -0,0 +1,369 @@ +# generated by datamodel-codegen: +# filename: + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel +from pydantic import Field + + +class AuthStatement(JsonModel): + actions: Optional[List[str]] = None + """ + A list of actions the user is allowed to perform + """ + resource: Optional[List[str]] = None + """ + A list of resources the user is allowed to access + """ + workspace: Optional[str] = Field(None, example="workspace-id") + """ + The workspace the user is allowed to access + """ + + +class AuthPolicy(JsonModel): + statements: Optional[List[AuthStatement]] = None + """ + A list of statements defining the actions the user can perform on a resource in a workspace + """ + + +class Statement(JsonModel): + actions: Optional[List[str]] = None + """ + A list of actions the user is allowed to perform + """ + resource: Optional[List[str]] = None + """ + A list of resources the user is allowed to access + """ + workspace: Optional[str] = Field(None, example="workspace-id") + """ + The workspace the user is allowed to access + """ + description: Optional[str] = None + """ + A description for this statement + """ + + +class Policy(JsonModel): + id: Optional[str] = Field(None, example="policy-id") + """ + The unique id + """ + name: Optional[str] = Field(None, example="policy-name") + """ + The policies's name + """ + type: Optional[str] = Field(None, example="role") + """ + The type of the policy + """ + built_in: Optional[bool] = Field(None, alias="builtIn", example=True) + """ + Whether the policy is built-in + """ + user_id: Optional[str] = Field(None, alias="userId", example="user-id") + """ + The user id + """ + created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The created timestamp + """ + updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The last updated timestamp + """ + deleted: Optional[bool] = Field(None, example=True) + """ + Whether the policy is deleted or not + """ + properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) + """ + A map of key value properties + """ + statements: Optional[List[Statement]] = None + """ + A list of statements defining the actions the user can perform on a resource in a workspace + """ + template_id: Optional[str] = Field(None, alias="templateId", example="policy-template-id") + """ + The id of the policy template. Only set if the policy has been created based on a template and + does not contain inline statements. + """ + workspace: Optional[str] = Field(None, example="workspace-id") + """ + The workspace the policy template applies to. Only set if the policy has been created based on a + template and does not contain inline statements. + """ + + +class Key(JsonModel): + id: Optional[str] = Field(None, example="api-key-id") + """ + The unique id + """ + name: Optional[str] = Field(None, example="api-key-name") + """ + The key's name + """ + user_id: Optional[str] = Field(None, alias="userId", example="user-id") + """ + The user id + """ + created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The created timestamp + """ + updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The last updated timestamp + """ + expiry: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The time when the key expires (epoch in milliseconds) + """ + enabled: Optional[bool] = Field(None, example=True) + """ + Whether the key is enabled or not + """ + deleted: Optional[bool] = Field(None, example=True) + """ + Whether the key is deleted or not + """ + default_workspace: Optional[str] = Field( + None, alias="defaultWorkspace", example="workspace-id" + ) + """ + This field overrides the default workspace when authenticating. + """ + properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) + """ + A map of key value properties + """ + policies: Optional[List[Policy]] = None + """ + A list of policy definitions including statements and permissions + """ + + +class Status(Enum): + PENDING = "pending" + ACTIVE = "active" + + +class User(JsonModel): + id: Optional[str] = Field(None, example="user-id") + """ + The unique id + """ + first_name: Optional[str] = Field( + None, alias="firstName", example="user-first-name" + ) + """ + The user's first name + """ + last_name: Optional[str] = Field(None, alias="lastName", example="user-last-name") + """ + The user's last name + """ + email: Optional[str] = Field(None, example="example@email.com") + """ + The user's email + """ + phone: Optional[str] = Field(None, example="555-555-5555") + """ + The user's contact phone number + """ + niua_id: Optional[str] = Field(None, alias="niuaId", example="example@email.com") + """ + The external id (niuaId, SID, login name) + """ + login: Optional[str] = None + """ + The login name of the user. This the "username" or equivalent entered when + the user authenticates with the identity provider. + """ + accepted_to_s: Optional[bool] = Field(None, alias="acceptedToS", example=True) + """ + (deprecated) Whether the user accepted the terms of service + """ + properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) + """ + A map of key value properties + """ + keywords: Optional[List[str]] = None + """ + A list of keywords associated with the user + """ + created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The created timestamp + """ + updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The last updated timestamp + """ + org_id: Optional[str] = Field(None, alias="orgId", example="org-id") + """ + The id of the organization + """ + policies: Optional[List[str]] = None + """ + A list of policy ids to reference existing policies + """ + status: Optional[Status] = Field(None, example="active") + """ + The status of the users' registration + """ + entitlements: Optional[Any] = None + """ + (deprecated) Features to which the user is entitled within the application. + """ + + +class Org(JsonModel): + id: Optional[str] = Field(None, example="org-id") + """ + The unique id + """ + name: Optional[str] = Field(None, example="org-name") + """ + The name of the organization + """ + owner_id: Optional[str] = Field(None, alias="ownerId", example="user-id") + """ + The userId of the organization owner + """ + + +class Workspace(JsonModel): + id: Optional[str] = Field(None, example="workspace-id") + """ + The unique id + """ + name: Optional[str] = Field(None, example="workspace-name") + """ + The workspace name + """ + enabled: Optional[bool] = Field(None, example=True) + """ + Whether the workspace is enabled or not + """ + default: Optional[bool] = Field(None, example=True) + """ + Whether the workspace is the default. The default workspace is used when callers omit a \ +workspace id + """ + + +class Error(JsonModel): + name: Optional[str] = None + """ + String error code + """ + code: Optional[int] = None + """ + Numeric error code + """ + resource_type: Optional[str] = Field(None, alias="resourceType") + """ + Type of resource associated with the error + """ + resource_id: Optional[str] = Field(None, alias="resourceId") + """ + Identifier of the resource associated with the error + """ + message: Optional[str] = None + """ + Complete error message + """ + args: Optional[List[str]] = None + """ + Positional argument values for the error code + """ + inner_errors: Optional[List[Error]] = Field(None, alias="innerErrors") + + +class UserPolicy(JsonModel): + id: Optional[str] = Field(None, example="policy-id") + """ + The identifier of a policy + """ + name: Optional[str] = Field(None, example="policy-name") + """ + The policy name + """ + template_id: Optional[str] = Field( + None, alias="templateId", example="policy-template-id" + ) + """ + The identifier of a policy template + """ + workspace_id: Optional[str] = Field( + None, alias="workspaceId", example="workspace-id" + ) + """ + The identifier of a workspace + """ + + +class UserPolicyTemplate(JsonModel): + id: Optional[str] = Field(None, example="policy-template-id") + """ + The identifier of a policy template + """ + name: Optional[str] = Field(None, example="policy-template-name") + """ + The policy template name + """ + + +class PolicyTemplate(JsonModel): + id: Optional[str] = Field(None, example="policy-template-id") + """ + The unique id + """ + name: Optional[str] = Field(None, example="policy-template-name") + """ + The policy template's name + """ + type: Optional[str] = Field(None, example="user") + """ + The type of the policy template + """ + built_in: Optional[bool] = Field(None, alias="builtIn", example=True) + """ + Whether the policy template is built-in + """ + user_id: Optional[str] = Field(None, alias="userId", example="user-id") + """ + The user id + """ + created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The created timestamp + """ + updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") + """ + The last updated timestamp + """ + deleted: Optional[bool] = Field(None, example=True) + """ + Whether the policy is deleted or not + """ + properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) + """ + A map of key value properties + """ + statements: Optional[List[Statement]] = None + """ + A list of statements defining the actions the user can perform on a resource + """ diff --git a/nisystemlink/clients/feeds/feeds_client.py b/nisystemlink/clients/feeds/feeds_client.py new file mode 100644 index 00000000..806961f9 --- /dev/null +++ b/nisystemlink/clients/feeds/feeds_client.py @@ -0,0 +1,81 @@ +"""Implementation of SystemLink Feeds Client.""" + +from typing import Optional + + +from nisystemlink.clients import core +from nisystemlink.clients.core._uplink._base_client import BaseClient +from nisystemlink.clients.core._uplink._methods import get, post +from nisystemlink.clients.feeds import models +from uplink import Body, Part, Path, Query, multipart + + +class SystemLinkFeedsClient(BaseClient): + """Class contains a set of methods to access the APIs of SystemLink Feed Client.""" + + def __init__(self, configuration: Optional[core.HttpConfiguration] = None): + """Initialize an instance. + + Args: + configuration: Defines the web server to connect to and information about + how to connect. If not provided, the + :class:`HttpConfigurationManager ` # noqa: W505 + is used to obtain the configuration. + + Raises: + ApiException: if unable to communicate with the Feeds Service. + """ + if configuration is None: + configuration = core.JupyterHttpConfiguration() + + super().__init__(configuration, base_path="/nifeed/v1/") + + @post("feeds") + def create_feed(self, feed: models.CreateFeedRequest) -> models.CreateOrUpdateFeedResponse: + """Create a new feed with the provided feed details. + + Args: + feeds (models.CreateFeedsRequest): Request model to create the feed. + + Returns: + models.CreateorUpdateFeedsResponse: Feed details of the newly created feed. + """ + ... + + @get("feeds", args=[Query, Query]) + def query_feeds( + self, + platform: Optional[str] = None, + workspace: Optional[str] = None, + ) -> models.FeedsQueryResponse: + """Get a set of feeds based on the provided `platform` and `workspace`. + + Args: + platform (Optional[str]): Information about system platform. Defaults to None. + workspace (Optional[str]): Workspace id. Defaults to None. + + Returns: + models.FeedsQueryResponse: List of feeds. + """ + ... + + @multipart + @post("feeds/{feedId}/packages", args=[Path(name="feedId"), Body]) + def upload_package( + self, + feed_id: str, + package: Part, + overwrite: Query(name="shouldOverwrite") = False, + ) -> models.UploadPackageResponse: + """Upload package to feeds. + + Args: + feed_id (str): ID of the feed. + package (Part): Package file as a form data. + Example: `package=open(filename, "rb")` + overwrite (Query(name="shouldOverwrite")): To overwrite the package if exists. Defaults to false. # noqa: W505 + + Returns: + models.UploadPackageResponse: Upload package response. + """ + ... diff --git a/nisystemlink/clients/feeds/models/__init__.py b/nisystemlink/clients/feeds/models/__init__.py new file mode 100644 index 00000000..4b2cc496 --- /dev/null +++ b/nisystemlink/clients/feeds/models/__init__.py @@ -0,0 +1,10 @@ +# flake8: noqa W505 +# Import the necessary models for Feed Service. + +from ._feeds_models import ( + CreateFeedRequest, + CreateOrUpdateFeedResponse, + FeedsQueryResponse, + Platform, + UploadPackageResponse, +) diff --git a/nisystemlink/clients/feeds/models/_feeds_models.py b/nisystemlink/clients/feeds/models/_feeds_models.py new file mode 100644 index 00000000..901c042b --- /dev/null +++ b/nisystemlink/clients/feeds/models/_feeds_models.py @@ -0,0 +1,87 @@ +"""Models utilized for Feeds in SystemLink.""" + +from __future__ import annotations + +from enum import Enum +from typing import Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel +from pydantic import Field + + +class Platform(Enum): + """Platform.""" + + WINDOWS = "WINDOWS" + NI_LINUX_RT = "NI_LINUX_RT" + + +class CreateFeedRequest(JsonModel): + """Create Feed Request.""" + + name: str + description: Optional[str] = None + platform: Platform + workspace: Optional[str] = None + + +class CreateOrUpdateFeedResponse(JsonModel): + """Create or Update Feed Response.""" + + id: Optional[str] = None + name: Optional[str] = None + description: Optional[str] = None + platform: Platform + workspace: Optional[str] = None + updated_at: str = Field(alias="updatedAt") + created_at: str = Field(alias="createdAt") + package_sources: Optional[List[str]] = Field(default=None, alias="packageSources") + deleted: bool + + +class FeedsQueryResponse(JsonModel): + """Query Feeds response.""" + + feeds: List[CreateOrUpdateFeedResponse] + + +class PackageMetadata(JsonModel): + """Package Meta data.""" + + package_name: Optional[str] = Field(default=None, alias="packageName") + version: Optional[str] = None + architecture: Optional[str] = None + breaks: Optional[List[str]] = None + conflicts: Optional[List[str]] = None + depends: Optional[List[str]] = None + description: Optional[str] = None + enhances: Optional[List[str]] = None + essential: Optional[bool] = None + file_name: Optional[str] = Field(default=None, alias="fileName") + homepage: Optional[str] = None + installed_size: Optional[int] = Field(default=None, alias="installedSize") + maintainer: Optional[str] = None + predepends: Optional[List[str]] = None + priority: int + provides: Optional[List[str]] = None + recommends: Optional[List[str]] = None + release_notes: Optional[str] = Field(default=None, alias="releaseNotes") + replaces: Optional[List[str]] = None + section: Optional[str] = None + size: Optional[int] = None + source: Optional[str] = None + suggests: Optional[List[str]] = None + tags: Optional[str] = None + attributes: Optional[Dict[str, str]] = None + + +class UploadPackageResponse(JsonModel): + """Upload package response.""" + + id: Optional[str] = None + file_name: Optional[str] = Field(default=None, alias="fileName") + feed_id: Optional[str] = Field(default=None, alias="feedId") + workspace: Optional[str] = None + updated_at: str = Field(alias="updatedAt") + created_at: str = Field(alias="createdAt") + metadata: Optional[PackageMetadata] = None From 074f782ecb39c44683a0f2a574894dcb6ddb4fe2 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 16 Oct 2024 18:25:07 +0530 Subject: [PATCH 02/44] feat: separte function for query feeds --- helpers/feeds_wrapper.py | 41 +++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py index a432b944..81dfa546 100644 --- a/helpers/feeds_wrapper.py +++ b/helpers/feeds_wrapper.py @@ -1,7 +1,7 @@ """Helper functions for using systemlink feeds APIs.""" import os -from typing import List, Union +from typing import Dict, List, Union from nisystemlink.clients.auth import AuthClient from nisystemlink.clients.core import ApiException, HttpConfiguration @@ -61,6 +61,32 @@ def create_feed( return create_feed_response +def query_existing_feed_info( + client: SystemLinkFeedsClient, + platform: str, + workspace_id: str, +) -> Dict[str, str]: + """Query existing feeds information from systemlink. + + Args: + client (SystemLinkFeedsClient): Systemlink feeds Client._ + platform (str): Name of the platform. + workspace_id (str): Workspace ID. + + Returns: + Dict[str, str]: Existing feeds information. + """ + query_feeds = client.query_feeds( + platform=platform, + workspace=workspace_id, + ) + existing_feeds = {} + for feed in query_feeds.feeds: + existing_feeds[feed.name] = feed.id + + return existing_feeds + + def upload_single_package( package_path: str, workspace_name: str, @@ -93,14 +119,11 @@ def upload_single_package( server_url=server_url, ) - query_feeds = client.query_feeds( + existing_feeds = query_existing_feed_info( platform=platform, - workspace=workspace_id, + client=client, + workspace_id=workspace_id, ) - existing_feeds = {} - for feed in query_feeds.feeds: - existing_feeds[feed.name] = feed.id - if feed_name not in existing_feeds: feed_id = create_feed( feed_name=feed_name, @@ -120,7 +143,7 @@ def upload_single_package( return response.file_name - except (ApiException, Exception) as exp: + except ApiException as exp: return exp.message @@ -157,7 +180,7 @@ def upload_multiple_packages( feed_name=feed_name, ) - except (ApiException, Exception) as exp: + except ApiException as exp: failed_packages.append(exp.message) return failed_packages From 590d91c3edcd378c2079a3249f5649d0175953a4 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 17 Oct 2024 09:57:47 +0530 Subject: [PATCH 03/44] docs: Update doc strings --- helpers/feeds_wrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py index 81dfa546..871e8e2b 100644 --- a/helpers/feeds_wrapper.py +++ b/helpers/feeds_wrapper.py @@ -121,7 +121,7 @@ def upload_single_package( existing_feeds = query_existing_feed_info( platform=platform, - client=client, + client=client, workspace_id=workspace_id, ) if feed_name not in existing_feeds: @@ -154,7 +154,7 @@ def upload_multiple_packages( server_url: str, feed_name: str, overwrite: bool, -) -> str: +) -> List[str]: """Upload multiple packages to systemlink feeds. Args: @@ -166,7 +166,7 @@ def upload_multiple_packages( overwrite (bool): To overwrite the package if exists. Returns: - str: Upload package responses. + List[str]: Upload package responses. """ failed_packages = [] for package_path in package_paths: From f4f06e06feeb7e0383f71b0b83e76e2221801776 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 17 Oct 2024 10:08:46 +0530 Subject: [PATCH 04/44] fix: Update upload mulitple package function --- helpers/feeds_wrapper.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py index 871e8e2b..a4f771b9 100644 --- a/helpers/feeds_wrapper.py +++ b/helpers/feeds_wrapper.py @@ -168,19 +168,18 @@ def upload_multiple_packages( Returns: List[str]: Upload package responses. """ - failed_packages = [] + responses = [] + for package_path in package_paths: - try: + responses.append( upload_single_package( - package_path=package_path, - server_api_key=server_api_key, - server_url=server_url, - workspace_name=workspace_name, - overwrite=overwrite, - feed_name=feed_name, + package_path=package_path, + server_api_key=server_api_key, + server_url=server_url, + workspace_name=workspace_name, + overwrite=overwrite, + feed_name=feed_name, ) + ) - except ApiException as exp: - failed_packages.append(exp.message) - - return failed_packages + return responses From 5d80d06a5a451f534f53de849831b0829746ed86 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Mon, 21 Oct 2024 13:33:43 +0530 Subject: [PATCH 05/44] fix: errors due to mypy checks --- examples/auth/get_workspace_id.py | 19 ++++++++++--------- examples/feeds/create_feed_apis.py | 12 +++++++----- examples/feeds/upload_package_apis.py | 18 ++++++++++-------- helpers/feeds_wrapper.py | 2 +- .../clients/auth/models/_auth_models.py | 4 +++- nisystemlink/clients/feeds/__init__.py | 3 +++ .../{feeds_client.py => _feeds_client.py} | 15 ++++++++------- nisystemlink/clients/feeds/models/__init__.py | 5 ++--- 8 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 nisystemlink/clients/feeds/__init__.py rename nisystemlink/clients/feeds/{feeds_client.py => _feeds_client.py} (86%) diff --git a/examples/auth/get_workspace_id.py b/examples/auth/get_workspace_id.py index 8c3177af..17ace1b0 100644 --- a/examples/auth/get_workspace_id.py +++ b/examples/auth/get_workspace_id.py @@ -3,21 +3,22 @@ from nisystemlink.clients.auth import AuthClient from nisystemlink.clients.core import ApiException, HttpConfiguration +server_url = "" # SystemLink API URL +server_api_key = "" # SystemLink API key +workspace_name = "" # Systemlink workspace name -server_url = None # SystemLink API URL -server_api_key = None # SystemLink API key -workspace_name = None # Systemlink workspace name - - -auth_client = AuthClient(HttpConfiguration(server_uri=server_url, api_key=server_api_key)) +auth_client = AuthClient( + HttpConfiguration(server_uri=server_url, api_key=server_api_key) +) try: caller_info = auth_client.authenticate() workspaces_info = caller_info.workspaces - for workspace_info in workspaces_info: - if workspace_info.name == workspace_name: - print(workspace_info.id) + if workspaces_info: + for workspace_info in workspaces_info: + if workspace_info.name == workspace_name: + print(workspace_info.id) except ApiException as exp: print(exp) diff --git a/examples/feeds/create_feed_apis.py b/examples/feeds/create_feed_apis.py index 19345a29..3cf2fd7d 100644 --- a/examples/feeds/create_feed_apis.py +++ b/examples/feeds/create_feed_apis.py @@ -1,7 +1,7 @@ """Functionality of creating feeds APIs.""" from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds.feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds._feeds_client import SystemLinkFeedsClient from nisystemlink.clients.feeds.models import ( CreateFeedRequest, Platform, @@ -13,12 +13,14 @@ FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" PLATFORM = Platform.WINDOWS.value -server_url = None # SystemLink API URL -server_api_key = None # SystemLink API key -workspace_id = None # Systemlink workspace id +server_url = "" # SystemLink API URL +server_api_key = "" # SystemLink API key +workspace_id = "" # Systemlink workspace id # Please provide the valid API key and API URL for client intialization. -client = SystemLinkFeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) +client = SystemLinkFeedsClient( + HttpConfiguration(api_key=server_api_key, server_uri=server_url) +) # Creating Feeds. try: diff --git a/examples/feeds/upload_package_apis.py b/examples/feeds/upload_package_apis.py index 319de5a4..704f02cd 100644 --- a/examples/feeds/upload_package_apis.py +++ b/examples/feeds/upload_package_apis.py @@ -1,7 +1,8 @@ """Functionality of uploading & querying feeds APIs.""" +from typing import Dict, Optional from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds.feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds._feeds_client import SystemLinkFeedsClient from nisystemlink.clients.feeds.models import Platform @@ -12,12 +13,14 @@ PACKAGE_NAME = "" PACKAGE_PATH = "" -server_url = None # SystemLink API URL -server_api_key = None # SystemLink API key -workspace_id = None # Systemlink workspace id +server_url = "" # SystemLink API URL +server_api_key = "" # SystemLink API key +workspace_id = "" # Systemlink workspace id # Please provide the valid API key and API URL for client intialization. -client = SystemLinkFeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) +client = SystemLinkFeedsClient( + HttpConfiguration(api_key=server_api_key, server_uri=server_url) +) # To upload a package to feed. try: @@ -26,10 +29,10 @@ platform=PLATFORM, workspace=workspace_id, ) - existing_feeds = {} + existing_feeds: Dict[str, str] = {} feed_id = "" for feed in query_feeds.feeds: - if feed.name == FEED_NAME: + if feed.name == FEED_NAME and feed.id: feed_id = feed.id break @@ -39,7 +42,6 @@ package=(PACKAGE_NAME, open(PACKAGE_PATH, "rb"), "multipart/form-data"), ) print("Package uploaded sucessfully.") - print(f"Upload package: {upload_package.metadata.package_name}") except ApiException as exp: print(exp) diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py index a4f771b9..aa722b85 100644 --- a/helpers/feeds_wrapper.py +++ b/helpers/feeds_wrapper.py @@ -5,7 +5,7 @@ from nisystemlink.clients.auth import AuthClient from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds.feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds._feeds_client import SystemLinkFeedsClient from nisystemlink.clients.feeds.models import ( CreateFeedRequest, CreateOrUpdateFeedResponse, diff --git a/nisystemlink/clients/auth/models/_auth_models.py b/nisystemlink/clients/auth/models/_auth_models.py index 6f86aa43..ea4267c7 100644 --- a/nisystemlink/clients/auth/models/_auth_models.py +++ b/nisystemlink/clients/auth/models/_auth_models.py @@ -93,7 +93,9 @@ class Policy(JsonModel): """ A list of statements defining the actions the user can perform on a resource in a workspace """ - template_id: Optional[str] = Field(None, alias="templateId", example="policy-template-id") + template_id: Optional[str] = Field( + None, alias="templateId", example="policy-template-id" + ) """ The id of the policy template. Only set if the policy has been created based on a template and does not contain inline statements. diff --git a/nisystemlink/clients/feeds/__init__.py b/nisystemlink/clients/feeds/__init__.py new file mode 100644 index 00000000..17e45c4b --- /dev/null +++ b/nisystemlink/clients/feeds/__init__.py @@ -0,0 +1,3 @@ +from ._feeds_client import SystemLinkFeedsClient + +# flake8: noqa diff --git a/nisystemlink/clients/feeds/feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py similarity index 86% rename from nisystemlink/clients/feeds/feeds_client.py rename to nisystemlink/clients/feeds/_feeds_client.py index 806961f9..01d5db25 100644 --- a/nisystemlink/clients/feeds/feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -2,12 +2,12 @@ from typing import Optional - from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient from nisystemlink.clients.core._uplink._methods import get, post -from nisystemlink.clients.feeds import models -from uplink import Body, Part, Path, Query, multipart +from uplink import Body, Part, Path, Query + +from . import models class SystemLinkFeedsClient(BaseClient): @@ -31,7 +31,9 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None): super().__init__(configuration, base_path="/nifeed/v1/") @post("feeds") - def create_feed(self, feed: models.CreateFeedRequest) -> models.CreateOrUpdateFeedResponse: + def create_feed( + self, feed: models.CreateFeedRequest + ) -> models.CreateOrUpdateFeedResponse: """Create a new feed with the provided feed details. Args: @@ -59,13 +61,12 @@ def query_feeds( """ ... - @multipart @post("feeds/{feedId}/packages", args=[Path(name="feedId"), Body]) def upload_package( self, feed_id: str, package: Part, - overwrite: Query(name="shouldOverwrite") = False, + overwrite: bool = Query[bool](False, name="shouldOverwrite"), ) -> models.UploadPackageResponse: """Upload package to feeds. @@ -73,7 +74,7 @@ def upload_package( feed_id (str): ID of the feed. package (Part): Package file as a form data. Example: `package=open(filename, "rb")` - overwrite (Query(name="shouldOverwrite")): To overwrite the package if exists. Defaults to false. # noqa: W505 + overwrite (bool): To overwrite the package if exists. Defaults to false. # noqa: W505 Returns: models.UploadPackageResponse: Upload package response. diff --git a/nisystemlink/clients/feeds/models/__init__.py b/nisystemlink/clients/feeds/models/__init__.py index 4b2cc496..8ffcd563 100644 --- a/nisystemlink/clients/feeds/models/__init__.py +++ b/nisystemlink/clients/feeds/models/__init__.py @@ -1,6 +1,3 @@ -# flake8: noqa W505 -# Import the necessary models for Feed Service. - from ._feeds_models import ( CreateFeedRequest, CreateOrUpdateFeedResponse, @@ -8,3 +5,5 @@ Platform, UploadPackageResponse, ) + +# flake8: noqa From 7dda9c7a25c506038cb2c99a2f3f0d117496f6bf Mon Sep 17 00:00:00 2001 From: Giriharan Date: Mon, 21 Oct 2024 16:24:39 +0530 Subject: [PATCH 06/44] docs: Update rst files for auth and feeds client --- docs/api_reference/auth.rst | 15 ++++++++++ docs/api_reference/feeds.rst | 16 ++++++++++ docs/getting_started.rst | 58 ++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 docs/api_reference/auth.rst create mode 100644 docs/api_reference/feeds.rst diff --git a/docs/api_reference/auth.rst b/docs/api_reference/auth.rst new file mode 100644 index 00000000..7d857f8a --- /dev/null +++ b/docs/api_reference/auth.rst @@ -0,0 +1,15 @@ +.. _api_tag_page: + +nisystemlink.clients.auth +====================== + +.. autoclass:: nisystemlink.clients.auth.AuthClient + :exclude-members: __init__ + + .. automethod:: __init__ + .. automethod:: authenticate + + +.. automodule:: nisystemlink.clients.auth.models + :members: + :imported-members: \ No newline at end of file diff --git a/docs/api_reference/feeds.rst b/docs/api_reference/feeds.rst new file mode 100644 index 00000000..bbf2ad6b --- /dev/null +++ b/docs/api_reference/feeds.rst @@ -0,0 +1,16 @@ +.. _api_tag_page: + +nisystemlink.clients.feeds +====================== + +.. autoclass:: nisystemlink.clients.feeds.FeedsClient + :exclude-members: __init__ + + .. automethod:: __init__ + .. automethod:: create_feed + .. automethod:: query_feeds + .. automethod:: upload_package + +.. automodule:: nisystemlink.clients.feeds.models + :members: + :imported-members: \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 0efe9358..6559bb04 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -182,5 +182,63 @@ Examples Get the metadata of a File using its Id and download it. .. literalinclude:: ../examples/file/download_file.py + :language: python + :linenos: + + +Auth API +------- + +Overview +~~~~~~~~ + +The :class:`.AuthClient` class is the primary entry point of the Auth API. + +When constructing a :class:`.AuthClient`, you can pass an +:class:`.HttpConfiguration` (like one retrieved from the +:class:`.HttpConfigurationManager`), or let :class:`.AuthClient` use the +default connection. The default connection depends on your environment. + +With a :class:`.AuthClient` object, you can: + +* Get the information about the caller. + +Examples +~~~~~~~~ + +Get the workspace id for the workspace name. + +.. literalinclude:: ../examples/auth/get_workspace_id.py + :language: python + :linenos: + +Feeds API +------- + +Overview +~~~~~~~~ + +The :class:`.FeedsClient` class is the primary entry point of the File API. + +When constructing a :class:`.FeedsClient`, you can pass an +:class:`.HttpConfiguration` (like one retrieved from the +:class:`.HttpConfigurationManager`), or let :class:`.FeedsClient` use the +default connection. The default connection depends on your environment. + +With a :class:`.FeedsClient` object, you can: + +* Get the list of feeds, create feed and upload package to feeds + +Examples +~~~~~~~~ + +Create new feed in SystemLink. + +.. literalinclude:: ../examples/file/create_feed.py + :language: python + :linenos: + +Upload package to SystemLink feeds +.. literalinclude:: ../examples/file/upload_package.py :language: python :linenos: \ No newline at end of file From 785eb39040d9b6d841d050b52f93a5f61c815343 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Mon, 21 Oct 2024 16:25:43 +0530 Subject: [PATCH 07/44] fix: Update file names and client name for feeds client --- .../feeds/{create_feed_apis.py => create_feed.py} | 4 ++-- .../{upload_package_apis.py => upload_package.py} | 4 ++-- helpers/feeds_wrapper.py | 12 ++++++------ nisystemlink/clients/feeds/__init__.py | 2 +- nisystemlink/clients/feeds/_feeds_client.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) rename examples/feeds/{create_feed_apis.py => create_feed.py} (90%) rename examples/feeds/{upload_package_apis.py => upload_package.py} (92%) diff --git a/examples/feeds/create_feed_apis.py b/examples/feeds/create_feed.py similarity index 90% rename from examples/feeds/create_feed_apis.py rename to examples/feeds/create_feed.py index 3cf2fd7d..e017f8f8 100644 --- a/examples/feeds/create_feed_apis.py +++ b/examples/feeds/create_feed.py @@ -1,7 +1,7 @@ """Functionality of creating feeds APIs.""" from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds._feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import ( CreateFeedRequest, Platform, @@ -18,7 +18,7 @@ workspace_id = "" # Systemlink workspace id # Please provide the valid API key and API URL for client intialization. -client = SystemLinkFeedsClient( +client = FeedsClient( HttpConfiguration(api_key=server_api_key, server_uri=server_url) ) diff --git a/examples/feeds/upload_package_apis.py b/examples/feeds/upload_package.py similarity index 92% rename from examples/feeds/upload_package_apis.py rename to examples/feeds/upload_package.py index 704f02cd..f41a36f4 100644 --- a/examples/feeds/upload_package_apis.py +++ b/examples/feeds/upload_package.py @@ -2,7 +2,7 @@ from typing import Dict, Optional from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds._feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import Platform @@ -18,7 +18,7 @@ workspace_id = "" # Systemlink workspace id # Please provide the valid API key and API URL for client intialization. -client = SystemLinkFeedsClient( +client = FeedsClient( HttpConfiguration(api_key=server_api_key, server_uri=server_url) ) diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py index aa722b85..21adbbb0 100644 --- a/helpers/feeds_wrapper.py +++ b/helpers/feeds_wrapper.py @@ -5,7 +5,7 @@ from nisystemlink.clients.auth import AuthClient from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds._feeds_client import SystemLinkFeedsClient +from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import ( CreateFeedRequest, CreateOrUpdateFeedResponse, @@ -39,7 +39,7 @@ def create_feed( feed_name: str, platform: str, workspace_id: str, - client: SystemLinkFeedsClient + client: FeedsClient ) -> CreateOrUpdateFeedResponse: """Create new feed in systemlink. @@ -47,7 +47,7 @@ def create_feed( feed_name (str): Name of the feed. platform (str): Name of the platform. workspace_id (str): Workspace ID. - client (SystemLinkFeedsClient): Systemlink feeds Client. + client (FeedsClient): Systemlink feeds Client. Returns: CreateOrUpdateFeedResponse: Create feed response. @@ -62,14 +62,14 @@ def create_feed( def query_existing_feed_info( - client: SystemLinkFeedsClient, + client: FeedsClient, platform: str, workspace_id: str, ) -> Dict[str, str]: """Query existing feeds information from systemlink. Args: - client (SystemLinkFeedsClient): Systemlink feeds Client._ + client (FeedsClient): Systemlink feeds Client._ platform (str): Name of the platform. workspace_id (str): Workspace ID. @@ -110,7 +110,7 @@ def upload_single_package( """ try: platform = __get_feed_platform(package_path) - client = SystemLinkFeedsClient( + client = FeedsClient( HttpConfiguration(api_key=server_api_key, server_uri=server_url) ) workspace_id = __get_workspace_id( diff --git a/nisystemlink/clients/feeds/__init__.py b/nisystemlink/clients/feeds/__init__.py index 17e45c4b..c5da55bd 100644 --- a/nisystemlink/clients/feeds/__init__.py +++ b/nisystemlink/clients/feeds/__init__.py @@ -1,3 +1,3 @@ -from ._feeds_client import SystemLinkFeedsClient +from ._feeds_client import FeedsClient # flake8: noqa diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 01d5db25..91d8a9b3 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -10,7 +10,7 @@ from . import models -class SystemLinkFeedsClient(BaseClient): +class FeedsClient(BaseClient): """Class contains a set of methods to access the APIs of SystemLink Feed Client.""" def __init__(self, configuration: Optional[core.HttpConfiguration] = None): From 8392e0b639707ad211df97f2ca9c4d82759ed468 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 23 Oct 2024 00:50:30 +0530 Subject: [PATCH 08/44] feat: add pytest for feeds and auth apis --- examples/feeds/create_feed.py | 4 +- examples/feeds/upload_package.py | 7 +- nisystemlink/clients/feeds/_feeds_client.py | 6 +- tests/integration/auth/test_auth_client.py | 18 +++ tests/integration/feeds/test_feeds_client.py | 151 ++++++++++++++++++ ...sample-measurement_0.5.0_windows_x64.nipkg | Bin 0 -> 34236 bytes 6 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 tests/integration/auth/test_auth_client.py create mode 100644 tests/integration/feeds/test_feeds_client.py create mode 100644 tests/integration/feeds/test_files/sample-measurement_0.5.0_windows_x64.nipkg diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index e017f8f8..00bc102b 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -18,9 +18,7 @@ workspace_id = "" # Systemlink workspace id # Please provide the valid API key and API URL for client intialization. -client = FeedsClient( - HttpConfiguration(api_key=server_api_key, server_uri=server_url) -) +client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # Creating Feeds. try: diff --git a/examples/feeds/upload_package.py b/examples/feeds/upload_package.py index f41a36f4..d08e1eb8 100644 --- a/examples/feeds/upload_package.py +++ b/examples/feeds/upload_package.py @@ -1,5 +1,6 @@ """Functionality of uploading & querying feeds APIs.""" -from typing import Dict, Optional + +from typing import Dict from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient @@ -18,9 +19,7 @@ workspace_id = "" # Systemlink workspace id # Please provide the valid API key and API URL for client intialization. -client = FeedsClient( - HttpConfiguration(api_key=server_api_key, server_uri=server_url) -) +client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # To upload a package to feed. try: diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 91d8a9b3..34211fbd 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -2,6 +2,7 @@ from typing import Optional + from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient from nisystemlink.clients.core._uplink._methods import get, post @@ -66,7 +67,7 @@ def upload_package( self, feed_id: str, package: Part, - overwrite: bool = Query[bool](False, name="shouldOverwrite"), + overwrite: Query(name="shouldOverwrite") = False, ) -> models.UploadPackageResponse: """Upload package to feeds. @@ -74,7 +75,8 @@ def upload_package( feed_id (str): ID of the feed. package (Part): Package file as a form data. Example: `package=open(filename, "rb")` - overwrite (bool): To overwrite the package if exists. Defaults to false. # noqa: W505 + overwrite (Query(name="shouldOverwrite")): To overwrite the package if exists.\ +Defaults to false Returns: models.UploadPackageResponse: Upload package response. diff --git a/tests/integration/auth/test_auth_client.py b/tests/integration/auth/test_auth_client.py new file mode 100644 index 00000000..95f84103 --- /dev/null +++ b/tests/integration/auth/test_auth_client.py @@ -0,0 +1,18 @@ +"""Integration tests for AuthClient.""" + +import pytest # type: ignore +from nisystemlink.clients.auth import AuthClient + + +@pytest.fixture(scope="class") +def client(enterprise_config) -> AuthClient: + """Fixture to create a AuthClient instance.""" + return AuthClient(enterprise_config) + + +@pytest.mark.enterprise +@pytest.mark.integration +class TestAuthClient: + def test__authenticate(self, client: AuthClient): + response = client.authenticate() + assert response is not None diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py new file mode 100644 index 00000000..0b676cf0 --- /dev/null +++ b/tests/integration/feeds/test_feeds_client.py @@ -0,0 +1,151 @@ +"""Integration tests for FeedsClient.""" + +from pathlib import Path +from typing import Callable + +import pytest # type: ignore +from nisystemlink.clients.core import ApiException +from nisystemlink.clients.feeds import FeedsClient +from nisystemlink.clients.feeds.models import CreateFeedRequest, Platform + + +WINDOWS_FEED_NAME = "Sample Feed" +LINUX_FEED_NAME = "Test Feed" +WORKSPACE_ID = "" # Provide valid workspace id. +FEED_DESCRIPTION = "Sample feed for uploading packages" +INVALID_WORKSPACE_ID = "12345" +PACKAGE_PATH = ( + Path(__file__).parent.resolve() + / "test_files" + / "sample-measurement_0.5.0_windows_x64.nipkg" +) +INVALID_PACKAGE_PATH = Path(__file__).parent.resolve() + + +@pytest.fixture(scope="class") +def client(enterprise_config) -> FeedsClient: + """Fixture to create a FeedsClient instance.""" + return FeedsClient(enterprise_config) + + +@pytest.fixture(scope="class") +def create_feed(client: FeedsClient): + """Fixture to return a object that creates feed.""" + def _create_feed(feed): + response = client.create_feed(feed) + return response + + yield _create_feed + + +@pytest.fixture(scope="class") +def create_windows_feed_request(): + """Fixture to create a request body of create feed API for windows platform.""" + def _create_feed_request(): + feed_request = CreateFeedRequest( + name=WINDOWS_FEED_NAME, + platform=Platform.WINDOWS.value, + workspace=WORKSPACE_ID, + description=FEED_DESCRIPTION, + ) + return feed_request + + yield _create_feed_request + + +@pytest.fixture(scope="class") +def create_linux_feed_request(): + """Fixture to create a request body of create feed API for linux platform.""" + def _create_feed_request(): + feed_request = CreateFeedRequest( + name=LINUX_FEED_NAME, + platform=Platform.NI_LINUX_RT.value, + workspace=WORKSPACE_ID, + description=FEED_DESCRIPTION, + ) + return feed_request + + yield _create_feed_request + + +@pytest.fixture(scope="class") +def get_feed_id(client: FeedsClient): + """Fixture to return the Feed ID of the created new feed.""" + query_feeds_response = client.query_feeds( + workspace=WORKSPACE_ID, + platform=Platform.WINDOWS.value, + ) + + for feed in query_feeds_response.feeds: + if feed.name == WINDOWS_FEED_NAME: + return feed.id + + +@pytest.mark.enterprise +@pytest.mark.integration +class TestFeedsClient: + """Class contains a set of test methods to test SystemLink Feeds API.""" + + def test__create_feed_windows_platform( + self, + create_feed: Callable, + create_windows_feed_request: Callable, + ): + """Test the case of a completely successful create feed API for windows platform.""" + request_body = create_windows_feed_request() + response = create_feed(request_body) + + assert response.id is not None + assert response.workspace is not None + assert response.name == WINDOWS_FEED_NAME + assert response.platform == Platform.WINDOWS + assert response.description == FEED_DESCRIPTION + + def test__create_feed_linux_platform( + self, + create_feed: Callable, + create_linux_feed_request: Callable, + ): + """Test the case of a completely successful create feed API for Linux platform.""" + request_body = create_linux_feed_request() + response = create_feed(request_body) + + assert response.id is not None + assert response.workspace is not None + assert response.name == LINUX_FEED_NAME + assert response.platform == Platform.NI_LINUX_RT + assert response.description == FEED_DESCRIPTION + + def test__query_feeds_windows_platform(self, client: FeedsClient): + """Test the case for querying available feeds for windows platform.""" + response = client.query_feeds( + platform=Platform.WINDOWS.value, + workspace=WORKSPACE_ID, + ) + assert response is not None + + def test__query_feeds_linux_platform(self, client: FeedsClient): + """Test the case for querying available feeds for Linux platform.""" + response = client.query_feeds( + platform=Platform.NI_LINUX_RT.value, workspace=WORKSPACE_ID + ) + assert response is not None + + def test__query_feeds_invalid_workspace(self, client: FeedsClient): + """Test the case of query feeds API with invalid workspace id.""" + with pytest.raises(ApiException, match="UnauthorizedWorkspaceError"): + client.query_feeds(workspace=INVALID_WORKSPACE_ID) + + def test__upload_package(self, client: FeedsClient, get_feed_id: Callable): + """Test the case of upload package to feed.""" + response = client.upload_package( + package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id + ) + assert response is not None + + def test__upload_duplicate_package( + self, client: FeedsClient, get_feed_id: Callable + ): + """Test the case of upload package to feed with invalid path.""" + with pytest.raises(ApiException, match="DuplicatePackageError"): + client.upload_package(package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id) diff --git a/tests/integration/feeds/test_files/sample-measurement_0.5.0_windows_x64.nipkg b/tests/integration/feeds/test_files/sample-measurement_0.5.0_windows_x64.nipkg new file mode 100644 index 0000000000000000000000000000000000000000..a1ad596fea7c6bd6e106151ce880274d40bdeea0 GIT binary patch literal 34236 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 Date: Wed, 23 Oct 2024 15:05:00 +0530 Subject: [PATCH 09/44] fix: mypy and black issues --- nisystemlink/clients/feeds/_feeds_client.py | 13 +++++++------ tests/integration/feeds/test_feeds_client.py | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 34211fbd..b8405cee 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -2,11 +2,10 @@ from typing import Optional - from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient from nisystemlink.clients.core._uplink._methods import get, post -from uplink import Body, Part, Path, Query +from uplink import Part, Path, Query from . import models @@ -62,12 +61,15 @@ def query_feeds( """ ... - @post("feeds/{feedId}/packages", args=[Path(name="feedId"), Body]) + @post( + "feeds/{feedId}/packages", + args=[Path(name="feedId"), Query(name="ShouldOverwrite")], + ) def upload_package( self, feed_id: str, package: Part, - overwrite: Query(name="shouldOverwrite") = False, + overwrite: Query = False, ) -> models.UploadPackageResponse: """Upload package to feeds. @@ -75,8 +77,7 @@ def upload_package( feed_id (str): ID of the feed. package (Part): Package file as a form data. Example: `package=open(filename, "rb")` - overwrite (Query(name="shouldOverwrite")): To overwrite the package if exists.\ -Defaults to false + overwrite (Query): To overwrite the package if exists. Defaults to false. Returns: models.UploadPackageResponse: Upload package response. diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 0b676cf0..0eb15c39 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -31,6 +31,7 @@ def client(enterprise_config) -> FeedsClient: @pytest.fixture(scope="class") def create_feed(client: FeedsClient): """Fixture to return a object that creates feed.""" + def _create_feed(feed): response = client.create_feed(feed) return response @@ -41,6 +42,7 @@ def _create_feed(feed): @pytest.fixture(scope="class") def create_windows_feed_request(): """Fixture to create a request body of create feed API for windows platform.""" + def _create_feed_request(): feed_request = CreateFeedRequest( name=WINDOWS_FEED_NAME, @@ -56,6 +58,7 @@ def _create_feed_request(): @pytest.fixture(scope="class") def create_linux_feed_request(): """Fixture to create a request body of create feed API for linux platform.""" + def _create_feed_request(): feed_request = CreateFeedRequest( name=LINUX_FEED_NAME, @@ -119,8 +122,7 @@ def test__create_feed_linux_platform( def test__query_feeds_windows_platform(self, client: FeedsClient): """Test the case for querying available feeds for windows platform.""" response = client.query_feeds( - platform=Platform.WINDOWS.value, - workspace=WORKSPACE_ID, + platform=Platform.WINDOWS.value, workspace=WORKSPACE_ID ) assert response is not None @@ -136,16 +138,14 @@ def test__query_feeds_invalid_workspace(self, client: FeedsClient): with pytest.raises(ApiException, match="UnauthorizedWorkspaceError"): client.query_feeds(workspace=INVALID_WORKSPACE_ID) - def test__upload_package(self, client: FeedsClient, get_feed_id: Callable): + def test__upload_package(self, client: FeedsClient, get_feed_id: str): """Test the case of upload package to feed.""" response = client.upload_package( package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id ) assert response is not None - def test__upload_duplicate_package( - self, client: FeedsClient, get_feed_id: Callable - ): + def test__upload_duplicate_package(self, client: FeedsClient, get_feed_id: str): """Test the case of upload package to feed with invalid path.""" with pytest.raises(ApiException, match="DuplicatePackageError"): client.upload_package(package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id) From facf5915cc5f3420055697fa9019243a0d0c4677 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 23 Oct 2024 15:35:59 +0530 Subject: [PATCH 10/44] fix: remove hardcoded workspace in pytest for feeds --- tests/integration/feeds/test_feeds_client.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 0eb15c39..39679ac9 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -11,7 +11,6 @@ WINDOWS_FEED_NAME = "Sample Feed" LINUX_FEED_NAME = "Test Feed" -WORKSPACE_ID = "" # Provide valid workspace id. FEED_DESCRIPTION = "Sample feed for uploading packages" INVALID_WORKSPACE_ID = "12345" PACKAGE_PATH = ( @@ -47,7 +46,6 @@ def _create_feed_request(): feed_request = CreateFeedRequest( name=WINDOWS_FEED_NAME, platform=Platform.WINDOWS.value, - workspace=WORKSPACE_ID, description=FEED_DESCRIPTION, ) return feed_request @@ -63,7 +61,6 @@ def _create_feed_request(): feed_request = CreateFeedRequest( name=LINUX_FEED_NAME, platform=Platform.NI_LINUX_RT.value, - workspace=WORKSPACE_ID, description=FEED_DESCRIPTION, ) return feed_request @@ -74,10 +71,7 @@ def _create_feed_request(): @pytest.fixture(scope="class") def get_feed_id(client: FeedsClient): """Fixture to return the Feed ID of the created new feed.""" - query_feeds_response = client.query_feeds( - workspace=WORKSPACE_ID, - platform=Platform.WINDOWS.value, - ) + query_feeds_response = client.query_feeds(platform=Platform.WINDOWS.value) for feed in query_feeds_response.feeds: if feed.name == WINDOWS_FEED_NAME: @@ -121,16 +115,12 @@ def test__create_feed_linux_platform( def test__query_feeds_windows_platform(self, client: FeedsClient): """Test the case for querying available feeds for windows platform.""" - response = client.query_feeds( - platform=Platform.WINDOWS.value, workspace=WORKSPACE_ID - ) + response = client.query_feeds(platform=Platform.WINDOWS.value) assert response is not None def test__query_feeds_linux_platform(self, client: FeedsClient): """Test the case for querying available feeds for Linux platform.""" - response = client.query_feeds( - platform=Platform.NI_LINUX_RT.value, workspace=WORKSPACE_ID - ) + response = client.query_feeds(platform=Platform.NI_LINUX_RT.value) assert response is not None def test__query_feeds_invalid_workspace(self, client: FeedsClient): From c8aa6ebfe560ace11bfd249c0d62d1586b8a4500 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 23 Oct 2024 15:52:47 +0530 Subject: [PATCH 11/44] docs: update api ref rst file --- docs/api_reference.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index b9181186..6fced502 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -13,6 +13,8 @@ API Reference api_reference/dataframe api_reference/spec api_reference/file + api_reference/auth + api_reference/feed Indices and tables ------------------ From 37ebd693f4655bd6921157139bc3b50e7f268e4f Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 23 Oct 2024 17:30:07 +0530 Subject: [PATCH 12/44] docs: update getting started rst file --- docs/getting_started.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 6559bb04..8ee7c161 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -239,6 +239,7 @@ Create new feed in SystemLink. :linenos: Upload package to SystemLink feeds + .. literalinclude:: ../examples/file/upload_package.py :language: python - :linenos: \ No newline at end of file + :linenos: From 5d41864c518802a405d0e789addbcd2c6d6f3fbe Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 23 Oct 2024 22:43:11 +0530 Subject: [PATCH 13/44] fix: err due to invalid path in getting started.rst file --- docs/getting_started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 8ee7c161..5ef0664f 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -234,12 +234,12 @@ Examples Create new feed in SystemLink. -.. literalinclude:: ../examples/file/create_feed.py +.. literalinclude:: ../examples/feeds/create_feed.py :language: python :linenos: Upload package to SystemLink feeds -.. literalinclude:: ../examples/file/upload_package.py +.. literalinclude:: ../examples/feeds/upload_package.py :language: python :linenos: From 11beed35b1e0ac6fce806e6fde67a17dddc8805b Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 11:24:25 +0530 Subject: [PATCH 14/44] fix: remove unwanted helpers --- helpers/feeds_wrapper.py | 185 --------------------------------------- 1 file changed, 185 deletions(-) delete mode 100644 helpers/feeds_wrapper.py diff --git a/helpers/feeds_wrapper.py b/helpers/feeds_wrapper.py deleted file mode 100644 index 21adbbb0..00000000 --- a/helpers/feeds_wrapper.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Helper functions for using systemlink feeds APIs.""" - -import os -from typing import Dict, List, Union - -from nisystemlink.clients.auth import AuthClient -from nisystemlink.clients.core import ApiException, HttpConfiguration -from nisystemlink.clients.feeds._feeds_client import FeedsClient -from nisystemlink.clients.feeds.models import ( - CreateFeedRequest, - CreateOrUpdateFeedResponse, - Platform, -) -NIPKG = ".nipkg" - - -def __get_feed_platform(package_path: str) -> str: - _, pkg_ext = os.path.splitext(package_path) - - if pkg_ext == NIPKG: - return Platform.WINDOWS.value - - return Platform.NI_LINUX_RT.value - - -def __get_workspace_id( - workspace_name: str, server_api_key: str, server_url: str, -) -> Union[str, None]: - auth_client = AuthClient(HttpConfiguration(server_uri=server_url, api_key=server_api_key)) - caller_info = auth_client.authenticate() - workspaces_info = caller_info.workspaces - - for workspace_info in workspaces_info: - if workspace_info.name == workspace_name: - return workspace_info.id - - -def create_feed( - feed_name: str, - platform: str, - workspace_id: str, - client: FeedsClient -) -> CreateOrUpdateFeedResponse: - """Create new feed in systemlink. - - Args: - feed_name (str): Name of the feed. - platform (str): Name of the platform. - workspace_id (str): Workspace ID. - client (FeedsClient): Systemlink feeds Client. - - Returns: - CreateOrUpdateFeedResponse: Create feed response. - """ - create_feed_request = CreateFeedRequest( - name=feed_name, - workspace=workspace_id, - platform=platform, - ) - create_feed_response = client.create_feed(create_feed_request) - return create_feed_response - - -def query_existing_feed_info( - client: FeedsClient, - platform: str, - workspace_id: str, -) -> Dict[str, str]: - """Query existing feeds information from systemlink. - - Args: - client (FeedsClient): Systemlink feeds Client._ - platform (str): Name of the platform. - workspace_id (str): Workspace ID. - - Returns: - Dict[str, str]: Existing feeds information. - """ - query_feeds = client.query_feeds( - platform=platform, - workspace=workspace_id, - ) - existing_feeds = {} - for feed in query_feeds.feeds: - existing_feeds[feed.name] = feed.id - - return existing_feeds - - -def upload_single_package( - package_path: str, - workspace_name: str, - server_api_key: str, - server_url: str, - feed_name: str, - overwrite: bool, -) -> str: - """Upload package to `SystemLink` feeds. - - Args: - package_path (str): Path of the package file. - workspace_name (str): Workspace name. - server_api_key (str): Systemlink API Key. - server_url (str): Systemlink API URL. - feed_name (str): Name of the feed. - overwrite (bool): To overwrite the package if exists. Defaults to false. - - Returns: - str: Upload package response. - """ - try: - platform = __get_feed_platform(package_path) - client = FeedsClient( - HttpConfiguration(api_key=server_api_key, server_uri=server_url) - ) - workspace_id = __get_workspace_id( - workspace_name=workspace_name, - server_api_key=server_api_key, - server_url=server_url, - ) - - existing_feeds = query_existing_feed_info( - platform=platform, - client=client, - workspace_id=workspace_id, - ) - if feed_name not in existing_feeds: - feed_id = create_feed( - feed_name=feed_name, - workspace_id=workspace_id, - client=client, - platform=platform, - ) - else: - feed_id = existing_feeds[feed_name] - - package_name = os.path.basename(package_path) - response = client.upload_package( - feed_id=feed_id, - package=(package_name, open(package_path, "rb"), "multipart/form-data"), - overwrite=overwrite, - ) - - return response.file_name - - except ApiException as exp: - return exp.message - - -def upload_multiple_packages( - package_paths: List[str], - workspace_name: str, - server_api_key: str, - server_url: str, - feed_name: str, - overwrite: bool, -) -> List[str]: - """Upload multiple packages to systemlink feeds. - - Args: - package_paths (List[str]): List of package file paths. - workspace_name (str): Workspace name. - server_api_key (str): Systemlink API Key. - server_url (str): Systemlink API URL. - feed_name (str): Name of the feed. - overwrite (bool): To overwrite the package if exists. - - Returns: - List[str]: Upload package responses. - """ - responses = [] - - for package_path in package_paths: - responses.append( - upload_single_package( - package_path=package_path, - server_api_key=server_api_key, - server_url=server_url, - workspace_name=workspace_name, - overwrite=overwrite, - feed_name=feed_name, - ) - ) - - return responses From 0690f733be4b0d4db131881a6cb411d909e77f99 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 12:13:44 +0530 Subject: [PATCH 15/44] fix: Address self review comments --- examples/auth/get_workspace_id.py | 2 +- examples/feeds/create_feed.py | 5 ++-- ...d_package.py => query_and_upload_feeds.py} | 0 nisystemlink/clients/auth/models/__init__.py | 2 +- .../clients/auth/models/_auth_info.py | 23 ------------------- .../clients/auth/models/_auth_models.py | 18 +++++++++++++-- nisystemlink/clients/feeds/_feeds_client.py | 11 +++++---- tests/integration/auth/test_auth_client.py | 3 +++ 8 files changed, 29 insertions(+), 35 deletions(-) rename examples/feeds/{upload_package.py => query_and_upload_feeds.py} (100%) delete mode 100644 nisystemlink/clients/auth/models/_auth_info.py diff --git a/examples/auth/get_workspace_id.py b/examples/auth/get_workspace_id.py index 17ace1b0..57e91fb7 100644 --- a/examples/auth/get_workspace_id.py +++ b/examples/auth/get_workspace_id.py @@ -1,4 +1,4 @@ -"""Functionality of getting authentication information.""" +"""Functionality of getting workspace ID using the workspace name.""" from nisystemlink.clients.auth import AuthClient from nisystemlink.clients.core import ApiException, HttpConfiguration diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index 00bc102b..dfcff603 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -8,7 +8,6 @@ ) -# Constant FEED_NAME = "EXAMPLE FEED" FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" PLATFORM = Platform.WINDOWS.value @@ -28,10 +27,10 @@ platform=PLATFORM, workspace=workspace_id, ) - example_feed = client.create_feed(feed=feed_request).name + created_feed_name = client.create_feed(feed=feed_request).name print("Feeds created Successfully.") - print(f"Created feed name: {example_feed}") + print(f"Created feed name: {created_feed_name}") except ApiException as exp: print(exp) diff --git a/examples/feeds/upload_package.py b/examples/feeds/query_and_upload_feeds.py similarity index 100% rename from examples/feeds/upload_package.py rename to examples/feeds/query_and_upload_feeds.py diff --git a/nisystemlink/clients/auth/models/__init__.py b/nisystemlink/clients/auth/models/__init__.py index e57ed92d..978c2bde 100644 --- a/nisystemlink/clients/auth/models/__init__.py +++ b/nisystemlink/clients/auth/models/__init__.py @@ -1,3 +1,3 @@ -from ._auth_info import AuthInfo +from ._auth_models import AuthInfo # flake8: noqa diff --git a/nisystemlink/clients/auth/models/_auth_info.py b/nisystemlink/clients/auth/models/_auth_info.py deleted file mode 100644 index f0013011..00000000 --- a/nisystemlink/clients/auth/models/_auth_info.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations - -from typing import Dict, List, Optional - -from nisystemlink.clients.core._uplink._json_model import JsonModel -from pydantic import Field - -from ._auth_models import AuthPolicy, Org, User, Workspace - - -class AuthInfo(JsonModel): - """Information about the authenticated caller.""" - - user: Optional[User] - """Details of authenticated caller""" - org: Optional[Org] - """Organization of authenticated caller""" - workspaces: Optional[List[Workspace]] - """List of workspaces the authenticated caller has access""" - policies: Optional[List[AuthPolicy]] - """List of policies for the authenticated caller""" - properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """A map of key value properties""" diff --git a/nisystemlink/clients/auth/models/_auth_models.py b/nisystemlink/clients/auth/models/_auth_models.py index ea4267c7..15404a6e 100644 --- a/nisystemlink/clients/auth/models/_auth_models.py +++ b/nisystemlink/clients/auth/models/_auth_models.py @@ -1,5 +1,4 @@ -# generated by datamodel-codegen: -# filename: +"""Models utilized for Auth in SystemLink.""" from __future__ import annotations @@ -369,3 +368,18 @@ class PolicyTemplate(JsonModel): """ A list of statements defining the actions the user can perform on a resource """ + + +class AuthInfo(JsonModel): + """Information about the authenticated caller.""" + + user: Optional[User] + """Details of authenticated caller""" + org: Optional[Org] + """Organization of authenticated caller""" + workspaces: Optional[List[Workspace]] + """List of workspaces the authenticated caller has access""" + policies: Optional[List[AuthPolicy]] + """List of policies for the authenticated caller""" + properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) + """A map of key value properties""" diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index b8405cee..0e1cdc8d 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -11,7 +11,7 @@ class FeedsClient(BaseClient): - """Class contains a set of methods to access the APIs of SystemLink Feed Client.""" + """Class contains a set of methods to access the APIs of SystemLink Feeds Client.""" def __init__(self, configuration: Optional[core.HttpConfiguration] = None): """Initialize an instance. @@ -40,7 +40,7 @@ def create_feed( feeds (models.CreateFeedsRequest): Request model to create the feed. Returns: - models.CreateorUpdateFeedsResponse: Feed details of the newly created feed. + models.CreateorUpdateFeedsResponse: Details of the created feed. """ ... @@ -71,15 +71,16 @@ def upload_package( package: Part, overwrite: Query = False, ) -> models.UploadPackageResponse: - """Upload package to feeds. + """Upload package to SystemLink feed. Args: feed_id (str): ID of the feed. package (Part): Package file as a form data. Example: `package=open(filename, "rb")` - overwrite (Query): To overwrite the package if exists. Defaults to false. + overwrite (Query): Set to True to overwrite the package if it already exists.\ +Defaults to False. Returns: - models.UploadPackageResponse: Upload package response. + models.UploadPackageResponse: Uploaded package response information. """ ... diff --git a/tests/integration/auth/test_auth_client.py b/tests/integration/auth/test_auth_client.py index 95f84103..1edf7a4c 100644 --- a/tests/integration/auth/test_auth_client.py +++ b/tests/integration/auth/test_auth_client.py @@ -13,6 +13,9 @@ def client(enterprise_config) -> AuthClient: @pytest.mark.enterprise @pytest.mark.integration class TestAuthClient: + """Class contains a set of test methods to test SystemLink Auth API.""" + def test__authenticate(self, client: AuthClient): + """Test the case of getting caller information with SystemLink Credentials.""" response = client.authenticate() assert response is not None From 734429b375cc5b74fb42ee22a98f33911e1f1a9c Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 12:40:56 +0530 Subject: [PATCH 16/44] docs: alignment in the rst files --- docs/api_reference.rst | 2 +- docs/getting_started.rst | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 6fced502..ee3df9c3 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -14,7 +14,7 @@ API Reference api_reference/spec api_reference/file api_reference/auth - api_reference/feed + api_reference/feeds Indices and tables ------------------ diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 5ef0664f..bc9ebd69 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -158,7 +158,6 @@ Update and Delete Specifications :language: python :linenos: - File API ------- @@ -185,7 +184,6 @@ Get the metadata of a File using its Id and download it. :language: python :linenos: - Auth API ------- From 480e4101a3ef90d002a72f812f81144c18d9cac1 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 12:41:35 +0530 Subject: [PATCH 17/44] fix: address second set of self review comments --- .../clients/auth/models/_auth_models.py | 154 ------------------ nisystemlink/clients/feeds/_feeds_client.py | 2 +- tests/integration/auth/test_auth_client.py | 2 +- tests/integration/feeds/test_feeds_client.py | 4 +- 4 files changed, 4 insertions(+), 158 deletions(-) diff --git a/nisystemlink/clients/auth/models/_auth_models.py b/nisystemlink/clients/auth/models/_auth_models.py index 15404a6e..c9baca1f 100644 --- a/nisystemlink/clients/auth/models/_auth_models.py +++ b/nisystemlink/clients/auth/models/_auth_models.py @@ -106,55 +106,6 @@ class Policy(JsonModel): """ -class Key(JsonModel): - id: Optional[str] = Field(None, example="api-key-id") - """ - The unique id - """ - name: Optional[str] = Field(None, example="api-key-name") - """ - The key's name - """ - user_id: Optional[str] = Field(None, alias="userId", example="user-id") - """ - The user id - """ - created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The created timestamp - """ - updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The last updated timestamp - """ - expiry: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The time when the key expires (epoch in milliseconds) - """ - enabled: Optional[bool] = Field(None, example=True) - """ - Whether the key is enabled or not - """ - deleted: Optional[bool] = Field(None, example=True) - """ - Whether the key is deleted or not - """ - default_workspace: Optional[str] = Field( - None, alias="defaultWorkspace", example="workspace-id" - ) - """ - This field overrides the default workspace when authenticating. - """ - properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """ - A map of key value properties - """ - policies: Optional[List[Policy]] = None - """ - A list of policy definitions including statements and permissions - """ - - class Status(Enum): PENDING = "pending" ACTIVE = "active" @@ -265,111 +216,6 @@ class Workspace(JsonModel): """ -class Error(JsonModel): - name: Optional[str] = None - """ - String error code - """ - code: Optional[int] = None - """ - Numeric error code - """ - resource_type: Optional[str] = Field(None, alias="resourceType") - """ - Type of resource associated with the error - """ - resource_id: Optional[str] = Field(None, alias="resourceId") - """ - Identifier of the resource associated with the error - """ - message: Optional[str] = None - """ - Complete error message - """ - args: Optional[List[str]] = None - """ - Positional argument values for the error code - """ - inner_errors: Optional[List[Error]] = Field(None, alias="innerErrors") - - -class UserPolicy(JsonModel): - id: Optional[str] = Field(None, example="policy-id") - """ - The identifier of a policy - """ - name: Optional[str] = Field(None, example="policy-name") - """ - The policy name - """ - template_id: Optional[str] = Field( - None, alias="templateId", example="policy-template-id" - ) - """ - The identifier of a policy template - """ - workspace_id: Optional[str] = Field( - None, alias="workspaceId", example="workspace-id" - ) - """ - The identifier of a workspace - """ - - -class UserPolicyTemplate(JsonModel): - id: Optional[str] = Field(None, example="policy-template-id") - """ - The identifier of a policy template - """ - name: Optional[str] = Field(None, example="policy-template-name") - """ - The policy template name - """ - - -class PolicyTemplate(JsonModel): - id: Optional[str] = Field(None, example="policy-template-id") - """ - The unique id - """ - name: Optional[str] = Field(None, example="policy-template-name") - """ - The policy template's name - """ - type: Optional[str] = Field(None, example="user") - """ - The type of the policy template - """ - built_in: Optional[bool] = Field(None, alias="builtIn", example=True) - """ - Whether the policy template is built-in - """ - user_id: Optional[str] = Field(None, alias="userId", example="user-id") - """ - The user id - """ - created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The created timestamp - """ - updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The last updated timestamp - """ - deleted: Optional[bool] = Field(None, example=True) - """ - Whether the policy is deleted or not - """ - properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """ - A map of key value properties - """ - statements: Optional[List[Statement]] = None - """ - A list of statements defining the actions the user can perform on a resource - """ - - class AuthInfo(JsonModel): """Information about the authenticated caller.""" diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 0e1cdc8d..4e9b13aa 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -19,7 +19,7 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None): Args: configuration: Defines the web server to connect to and information about how to connect. If not provided, the - :class:`HttpConfigurationManager ` # noqa: W505 + :class:`HttpConfigurationManager ` is used to obtain the configuration. Raises: diff --git a/tests/integration/auth/test_auth_client.py b/tests/integration/auth/test_auth_client.py index 1edf7a4c..f890f0cb 100644 --- a/tests/integration/auth/test_auth_client.py +++ b/tests/integration/auth/test_auth_client.py @@ -1,6 +1,6 @@ """Integration tests for AuthClient.""" -import pytest # type: ignore +import pytest from nisystemlink.clients.auth import AuthClient diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 39679ac9..b74d7540 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Callable -import pytest # type: ignore +import pytest from nisystemlink.clients.core import ApiException from nisystemlink.clients.feeds import FeedsClient from nisystemlink.clients.feeds.models import CreateFeedRequest, Platform @@ -136,6 +136,6 @@ def test__upload_package(self, client: FeedsClient, get_feed_id: str): assert response is not None def test__upload_duplicate_package(self, client: FeedsClient, get_feed_id: str): - """Test the case of upload package to feed with invalid path.""" + """Test the case of uploading duplicate package to feed.""" with pytest.raises(ApiException, match="DuplicatePackageError"): client.upload_package(package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id) From 1c014935d00db79453128bf22776a499eb777082 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 12:51:29 +0530 Subject: [PATCH 18/44] docs: Update doc string of auth models --- .../clients/auth/models/_auth_models.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/nisystemlink/clients/auth/models/_auth_models.py b/nisystemlink/clients/auth/models/_auth_models.py index c9baca1f..33915cb7 100644 --- a/nisystemlink/clients/auth/models/_auth_models.py +++ b/nisystemlink/clients/auth/models/_auth_models.py @@ -11,6 +11,8 @@ class AuthStatement(JsonModel): + """Auth Statement information.""" + actions: Optional[List[str]] = None """ A list of actions the user is allowed to perform @@ -26,6 +28,8 @@ class AuthStatement(JsonModel): class AuthPolicy(JsonModel): + """Auth Policy information.""" + statements: Optional[List[AuthStatement]] = None """ A list of statements defining the actions the user can perform on a resource in a workspace @@ -33,6 +37,8 @@ class AuthPolicy(JsonModel): class Statement(JsonModel): + """Statement information.""" + actions: Optional[List[str]] = None """ A list of actions the user is allowed to perform @@ -52,6 +58,8 @@ class Statement(JsonModel): class Policy(JsonModel): + """Policy information.""" + id: Optional[str] = Field(None, example="policy-id") """ The unique id @@ -107,11 +115,15 @@ class Policy(JsonModel): class Status(Enum): + """Enumeration to represent different status of user's registration.""" + PENDING = "pending" ACTIVE = "active" class User(JsonModel): + """User information.""" + id: Optional[str] = Field(None, example="user-id") """ The unique id @@ -182,6 +194,8 @@ class User(JsonModel): class Org(JsonModel): + """User's Organization information.""" + id: Optional[str] = Field(None, example="org-id") """ The unique id @@ -197,6 +211,8 @@ class Org(JsonModel): class Workspace(JsonModel): + """Workspace information.""" + id: Optional[str] = Field(None, example="workspace-id") """ The unique id @@ -220,12 +236,22 @@ class AuthInfo(JsonModel): """Information about the authenticated caller.""" user: Optional[User] - """Details of authenticated caller""" + """ + Details of authenticated caller + """ org: Optional[Org] - """Organization of authenticated caller""" + """ + Organization of authenticated caller + """ workspaces: Optional[List[Workspace]] - """List of workspaces the authenticated caller has access""" + """ + List of workspaces the authenticated caller has access + """ policies: Optional[List[AuthPolicy]] - """List of policies for the authenticated caller""" + """ + List of policies for the authenticated caller + """ properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """A map of key value properties""" + """ + A map of key value properties + """ From fc06d3ab30b7dc8a338aaf7a642638d5a10c0be2 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 13:48:44 +0530 Subject: [PATCH 19/44] feat: add delete feed api --- examples/feeds/create_feed.py | 2 +- examples/feeds/delete_feed.py | 23 ++++++++++++ nisystemlink/clients/feeds/_feeds_client.py | 17 ++++++++- tests/integration/feeds/test_feeds_client.py | 38 +++++++++++++++----- 4 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 examples/feeds/delete_feed.py diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index dfcff603..6a2ad561 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -29,7 +29,7 @@ ) created_feed_name = client.create_feed(feed=feed_request).name - print("Feeds created Successfully.") + print("Feed created Successfully.") print(f"Created feed name: {created_feed_name}") except ApiException as exp: diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py new file mode 100644 index 00000000..d9b02a33 --- /dev/null +++ b/examples/feeds/delete_feed.py @@ -0,0 +1,23 @@ +"""Functionality of deleting feed API.""" + +from nisystemlink.clients.core import ApiException, HttpConfiguration +from nisystemlink.clients.feeds._feeds_client import FeedsClient + + +FEED_ID = "" + +server_url = "" # SystemLink API URL +server_api_key = "" # SystemLink API key + +# Please provide the valid API key and API URL for client intialization. +client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) + +# Deleting Feed. +try: + created_feed_name = client.delete_feed(feed_id=FEED_ID) + print("Feed deleted successfully.") + +except ApiException as exp: + print(exp) +except Exception as exp: + print(exp) diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 4e9b13aa..0169c629 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -4,7 +4,7 @@ from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient -from nisystemlink.clients.core._uplink._methods import get, post +from nisystemlink.clients.core._uplink._methods import delete, get, post from uplink import Part, Path, Query from . import models @@ -84,3 +84,18 @@ def upload_package( models.UploadPackageResponse: Uploaded package response information. """ ... + + @delete( + "feeds/{feedId}", + args=[Path(name="feedId")], + ) + def delete_feed(self, feed_id: str) -> None: + """Delete feed and its packages. + + Args: + feed_id (str): ID of the feed to be deleted. + + Returns: + None. + """ + ... diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index b74d7540..1dc8c993 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -9,8 +9,8 @@ from nisystemlink.clients.feeds.models import CreateFeedRequest, Platform -WINDOWS_FEED_NAME = "Sample Feed" -LINUX_FEED_NAME = "Test Feed" +WINDOWS_FEED_NAME = "Windows Feed" +LINUX_FEED_NAME = "Linux Feed" FEED_DESCRIPTION = "Sample feed for uploading packages" INVALID_WORKSPACE_ID = "12345" PACKAGE_PATH = ( @@ -69,8 +69,8 @@ def _create_feed_request(): @pytest.fixture(scope="class") -def get_feed_id(client: FeedsClient): - """Fixture to return the Feed ID of the created new feed.""" +def get_windows_feed_id(client: FeedsClient): + """Fixture to return the Feed ID of the created windows feed.""" query_feeds_response = client.query_feeds(platform=Platform.WINDOWS.value) for feed in query_feeds_response.feeds: @@ -78,6 +78,16 @@ def get_feed_id(client: FeedsClient): return feed.id +@pytest.fixture(scope="class") +def get_linux_feed_id(client: FeedsClient): + """Fixture to return the Feed ID of the created Linux feed.""" + query_feeds_response = client.query_feeds(platform=Platform.NI_LINUX_RT.value) + + for feed in query_feeds_response.feeds: + if feed.name == LINUX_FEED_NAME: + return feed.id + + @pytest.mark.enterprise @pytest.mark.integration class TestFeedsClient: @@ -119,7 +129,7 @@ def test__query_feeds_windows_platform(self, client: FeedsClient): assert response is not None def test__query_feeds_linux_platform(self, client: FeedsClient): - """Test the case for querying available feeds for Linux platform.""" + """Test the case for querying available feeds for linux platform.""" response = client.query_feeds(platform=Platform.NI_LINUX_RT.value) assert response is not None @@ -128,14 +138,24 @@ def test__query_feeds_invalid_workspace(self, client: FeedsClient): with pytest.raises(ApiException, match="UnauthorizedWorkspaceError"): client.query_feeds(workspace=INVALID_WORKSPACE_ID) - def test__upload_package(self, client: FeedsClient, get_feed_id: str): + def test__upload_package(self, client: FeedsClient, get_windows_feed_id: str): """Test the case of upload package to feed.""" response = client.upload_package( - package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id + package=open(PACKAGE_PATH, "rb"), feed_id=get_windows_feed_id, overwrite=True ) assert response is not None - def test__upload_duplicate_package(self, client: FeedsClient, get_feed_id: str): + def test__upload_duplicate_package(self, client: FeedsClient, get_windows_feed_id: str): """Test the case of uploading duplicate package to feed.""" with pytest.raises(ApiException, match="DuplicatePackageError"): - client.upload_package(package=open(PACKAGE_PATH, "rb"), feed_id=get_feed_id) + client.upload_package(package=open(PACKAGE_PATH, "rb"), feed_id=get_windows_feed_id) + + def test__delete__windows_feed(self, client: FeedsClient, get_windows_feed_id: str): + """Test the case of deleting windows feed with its packages.""" + response = client.delete_feed(feed_id=get_windows_feed_id) + assert response is None + + def test__delete__linux_feed(self, client: FeedsClient, get_linux_feed_id: str): + """Test the case of deleting linux feed with its packages.""" + response = client.delete_feed(feed_id=get_linux_feed_id) + assert response is None From 76bccbc293a0a082084d30148393e7e7f73677c0 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 13:54:34 +0530 Subject: [PATCH 20/44] fix: issues due to linting --- tests/integration/feeds/test_feeds_client.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 1dc8c993..6a0db8dc 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -141,14 +141,23 @@ def test__query_feeds_invalid_workspace(self, client: FeedsClient): def test__upload_package(self, client: FeedsClient, get_windows_feed_id: str): """Test the case of upload package to feed.""" response = client.upload_package( - package=open(PACKAGE_PATH, "rb"), feed_id=get_windows_feed_id, overwrite=True + package=open(PACKAGE_PATH, "rb"), + feed_id=get_windows_feed_id, + overwrite=True, ) assert response is not None - def test__upload_duplicate_package(self, client: FeedsClient, get_windows_feed_id: str): + def test__upload_duplicate_package( + self, + client: FeedsClient, + get_windows_feed_id: str, + ): """Test the case of uploading duplicate package to feed.""" with pytest.raises(ApiException, match="DuplicatePackageError"): - client.upload_package(package=open(PACKAGE_PATH, "rb"), feed_id=get_windows_feed_id) + client.upload_package( + package=open(PACKAGE_PATH, "rb"), + feed_id=get_windows_feed_id, + ) def test__delete__windows_feed(self, client: FeedsClient, get_windows_feed_id: str): """Test the case of deleting windows feed with its packages.""" From c046b1a0552edd09a7d7ab4e3d9b30622511ed27 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 24 Oct 2024 15:15:19 +0530 Subject: [PATCH 21/44] docs: update rst files --- docs/api_reference/feeds.rst | 1 + docs/getting_started.rst | 14 ++++++++++---- tests/integration/feeds/test_feeds_client.py | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/api_reference/feeds.rst b/docs/api_reference/feeds.rst index bbf2ad6b..739045f9 100644 --- a/docs/api_reference/feeds.rst +++ b/docs/api_reference/feeds.rst @@ -10,6 +10,7 @@ nisystemlink.clients.feeds .. automethod:: create_feed .. automethod:: query_feeds .. automethod:: upload_package + .. automethod:: delete_feed .. automodule:: nisystemlink.clients.feeds.models :members: diff --git a/docs/getting_started.rst b/docs/getting_started.rst index bc9ebd69..d1fee3a8 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -225,19 +225,25 @@ default connection. The default connection depends on your environment. With a :class:`.FeedsClient` object, you can: -* Get the list of feeds, create feed and upload package to feeds +* Get the list of feeds, create feed, upload package to feed and delete feed. Examples ~~~~~~~~ -Create new feed in SystemLink. +Create new feed. .. literalinclude:: ../examples/feeds/create_feed.py :language: python :linenos: -Upload package to SystemLink feeds +Query feeds and Upload package to feed -.. literalinclude:: ../examples/feeds/upload_package.py +.. literalinclude:: ../examples/feeds/query_and_upload_feeds.py + :language: python + :linenos: + +Delete feed with its package + +.. literalinclude:: ../examples/feeds/delete_feed.py :language: python :linenos: diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 6a0db8dc..0d9b4297 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -9,8 +9,8 @@ from nisystemlink.clients.feeds.models import CreateFeedRequest, Platform -WINDOWS_FEED_NAME = "Windows Feed" -LINUX_FEED_NAME = "Linux Feed" +WINDOWS_FEED_NAME = "Windows feed" +LINUX_FEED_NAME = "Linux feed" FEED_DESCRIPTION = "Sample feed for uploading packages" INVALID_WORKSPACE_ID = "12345" PACKAGE_PATH = ( From c9bd9bb09dbf7f35db9b4555753c5a2b53f8b9e4 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Fri, 25 Oct 2024 11:38:23 +0530 Subject: [PATCH 22/44] fix: remove auth implementation --- docs/api_reference.rst | 1 - docs/api_reference/auth.rst | 15 - docs/getting_started.rst | 26 -- examples/auth/get_workspace_id.py | 27 -- nisystemlink/clients/auth/__init__.py | 3 - nisystemlink/clients/auth/_auth_client.py | 35 --- nisystemlink/clients/auth/models/__init__.py | 3 - .../clients/auth/models/_auth_models.py | 257 ------------------ tests/integration/auth/test_auth_client.py | 21 -- 9 files changed, 388 deletions(-) delete mode 100644 docs/api_reference/auth.rst delete mode 100644 examples/auth/get_workspace_id.py delete mode 100644 nisystemlink/clients/auth/__init__.py delete mode 100644 nisystemlink/clients/auth/_auth_client.py delete mode 100644 nisystemlink/clients/auth/models/__init__.py delete mode 100644 nisystemlink/clients/auth/models/_auth_models.py delete mode 100644 tests/integration/auth/test_auth_client.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index ee3df9c3..74064439 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -13,7 +13,6 @@ API Reference api_reference/dataframe api_reference/spec api_reference/file - api_reference/auth api_reference/feeds Indices and tables diff --git a/docs/api_reference/auth.rst b/docs/api_reference/auth.rst deleted file mode 100644 index 7d857f8a..00000000 --- a/docs/api_reference/auth.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. _api_tag_page: - -nisystemlink.clients.auth -====================== - -.. autoclass:: nisystemlink.clients.auth.AuthClient - :exclude-members: __init__ - - .. automethod:: __init__ - .. automethod:: authenticate - - -.. automodule:: nisystemlink.clients.auth.models - :members: - :imported-members: \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index d1fee3a8..a29ed3f6 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -184,32 +184,6 @@ Get the metadata of a File using its Id and download it. :language: python :linenos: -Auth API -------- - -Overview -~~~~~~~~ - -The :class:`.AuthClient` class is the primary entry point of the Auth API. - -When constructing a :class:`.AuthClient`, you can pass an -:class:`.HttpConfiguration` (like one retrieved from the -:class:`.HttpConfigurationManager`), or let :class:`.AuthClient` use the -default connection. The default connection depends on your environment. - -With a :class:`.AuthClient` object, you can: - -* Get the information about the caller. - -Examples -~~~~~~~~ - -Get the workspace id for the workspace name. - -.. literalinclude:: ../examples/auth/get_workspace_id.py - :language: python - :linenos: - Feeds API ------- diff --git a/examples/auth/get_workspace_id.py b/examples/auth/get_workspace_id.py deleted file mode 100644 index 57e91fb7..00000000 --- a/examples/auth/get_workspace_id.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Functionality of getting workspace ID using the workspace name.""" - -from nisystemlink.clients.auth import AuthClient -from nisystemlink.clients.core import ApiException, HttpConfiguration - -server_url = "" # SystemLink API URL -server_api_key = "" # SystemLink API key -workspace_name = "" # Systemlink workspace name - -auth_client = AuthClient( - HttpConfiguration(server_uri=server_url, api_key=server_api_key) -) - -try: - caller_info = auth_client.authenticate() - workspaces_info = caller_info.workspaces - - if workspaces_info: - for workspace_info in workspaces_info: - if workspace_info.name == workspace_name: - print(workspace_info.id) - -except ApiException as exp: - print(exp) - -except Exception as exp: - print(exp) diff --git a/nisystemlink/clients/auth/__init__.py b/nisystemlink/clients/auth/__init__.py deleted file mode 100644 index 5d80edbd..00000000 --- a/nisystemlink/clients/auth/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._auth_client import AuthClient - -# flake8: noqa diff --git a/nisystemlink/clients/auth/_auth_client.py b/nisystemlink/clients/auth/_auth_client.py deleted file mode 100644 index a0d8b4f3..00000000 --- a/nisystemlink/clients/auth/_auth_client.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Implementation of AuthClient.""" - -from typing import Optional - -from nisystemlink.clients import core -from nisystemlink.clients.core._uplink._base_client import BaseClient -from nisystemlink.clients.core._uplink._methods import get - -from . import models - - -class AuthClient(BaseClient): - """Class contains a set of methods to access the APIs of SystemLink Auth Client.""" - - def __init__(self, configuration: Optional[core.HttpConfiguration] = None): - """Initialize an instance. - - Args: - configuration: Defines the web server to connect to and information about - how to connect. If not provided, an instance of - :class:`JupyterHttpConfiguration ` # noqa: W505 - is used. - - Raises: - ApiException: if unable to communicate with the Auth Service. - """ - if configuration is None: - configuration = core.JupyterHttpConfiguration() - - super().__init__(configuration, base_path="/niauth/v1/") - - @get("auth") - def authenticate(self) -> models.AuthInfo: - """Authenticates the given x-ni-api-key and returns information about the caller.""" - ... diff --git a/nisystemlink/clients/auth/models/__init__.py b/nisystemlink/clients/auth/models/__init__.py deleted file mode 100644 index 978c2bde..00000000 --- a/nisystemlink/clients/auth/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._auth_models import AuthInfo - -# flake8: noqa diff --git a/nisystemlink/clients/auth/models/_auth_models.py b/nisystemlink/clients/auth/models/_auth_models.py deleted file mode 100644 index 33915cb7..00000000 --- a/nisystemlink/clients/auth/models/_auth_models.py +++ /dev/null @@ -1,257 +0,0 @@ -"""Models utilized for Auth in SystemLink.""" - -from __future__ import annotations - -from datetime import datetime -from enum import Enum -from typing import Any, Dict, List, Optional - -from nisystemlink.clients.core._uplink._json_model import JsonModel -from pydantic import Field - - -class AuthStatement(JsonModel): - """Auth Statement information.""" - - actions: Optional[List[str]] = None - """ - A list of actions the user is allowed to perform - """ - resource: Optional[List[str]] = None - """ - A list of resources the user is allowed to access - """ - workspace: Optional[str] = Field(None, example="workspace-id") - """ - The workspace the user is allowed to access - """ - - -class AuthPolicy(JsonModel): - """Auth Policy information.""" - - statements: Optional[List[AuthStatement]] = None - """ - A list of statements defining the actions the user can perform on a resource in a workspace - """ - - -class Statement(JsonModel): - """Statement information.""" - - actions: Optional[List[str]] = None - """ - A list of actions the user is allowed to perform - """ - resource: Optional[List[str]] = None - """ - A list of resources the user is allowed to access - """ - workspace: Optional[str] = Field(None, example="workspace-id") - """ - The workspace the user is allowed to access - """ - description: Optional[str] = None - """ - A description for this statement - """ - - -class Policy(JsonModel): - """Policy information.""" - - id: Optional[str] = Field(None, example="policy-id") - """ - The unique id - """ - name: Optional[str] = Field(None, example="policy-name") - """ - The policies's name - """ - type: Optional[str] = Field(None, example="role") - """ - The type of the policy - """ - built_in: Optional[bool] = Field(None, alias="builtIn", example=True) - """ - Whether the policy is built-in - """ - user_id: Optional[str] = Field(None, alias="userId", example="user-id") - """ - The user id - """ - created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The created timestamp - """ - updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The last updated timestamp - """ - deleted: Optional[bool] = Field(None, example=True) - """ - Whether the policy is deleted or not - """ - properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """ - A map of key value properties - """ - statements: Optional[List[Statement]] = None - """ - A list of statements defining the actions the user can perform on a resource in a workspace - """ - template_id: Optional[str] = Field( - None, alias="templateId", example="policy-template-id" - ) - """ - The id of the policy template. Only set if the policy has been created based on a template and - does not contain inline statements. - """ - workspace: Optional[str] = Field(None, example="workspace-id") - """ - The workspace the policy template applies to. Only set if the policy has been created based on a - template and does not contain inline statements. - """ - - -class Status(Enum): - """Enumeration to represent different status of user's registration.""" - - PENDING = "pending" - ACTIVE = "active" - - -class User(JsonModel): - """User information.""" - - id: Optional[str] = Field(None, example="user-id") - """ - The unique id - """ - first_name: Optional[str] = Field( - None, alias="firstName", example="user-first-name" - ) - """ - The user's first name - """ - last_name: Optional[str] = Field(None, alias="lastName", example="user-last-name") - """ - The user's last name - """ - email: Optional[str] = Field(None, example="example@email.com") - """ - The user's email - """ - phone: Optional[str] = Field(None, example="555-555-5555") - """ - The user's contact phone number - """ - niua_id: Optional[str] = Field(None, alias="niuaId", example="example@email.com") - """ - The external id (niuaId, SID, login name) - """ - login: Optional[str] = None - """ - The login name of the user. This the "username" or equivalent entered when - the user authenticates with the identity provider. - """ - accepted_to_s: Optional[bool] = Field(None, alias="acceptedToS", example=True) - """ - (deprecated) Whether the user accepted the terms of service - """ - properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """ - A map of key value properties - """ - keywords: Optional[List[str]] = None - """ - A list of keywords associated with the user - """ - created: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The created timestamp - """ - updated: Optional[datetime] = Field(None, example="2019-12-02T15:31:45.379Z") - """ - The last updated timestamp - """ - org_id: Optional[str] = Field(None, alias="orgId", example="org-id") - """ - The id of the organization - """ - policies: Optional[List[str]] = None - """ - A list of policy ids to reference existing policies - """ - status: Optional[Status] = Field(None, example="active") - """ - The status of the users' registration - """ - entitlements: Optional[Any] = None - """ - (deprecated) Features to which the user is entitled within the application. - """ - - -class Org(JsonModel): - """User's Organization information.""" - - id: Optional[str] = Field(None, example="org-id") - """ - The unique id - """ - name: Optional[str] = Field(None, example="org-name") - """ - The name of the organization - """ - owner_id: Optional[str] = Field(None, alias="ownerId", example="user-id") - """ - The userId of the organization owner - """ - - -class Workspace(JsonModel): - """Workspace information.""" - - id: Optional[str] = Field(None, example="workspace-id") - """ - The unique id - """ - name: Optional[str] = Field(None, example="workspace-name") - """ - The workspace name - """ - enabled: Optional[bool] = Field(None, example=True) - """ - Whether the workspace is enabled or not - """ - default: Optional[bool] = Field(None, example=True) - """ - Whether the workspace is the default. The default workspace is used when callers omit a \ -workspace id - """ - - -class AuthInfo(JsonModel): - """Information about the authenticated caller.""" - - user: Optional[User] - """ - Details of authenticated caller - """ - org: Optional[Org] - """ - Organization of authenticated caller - """ - workspaces: Optional[List[Workspace]] - """ - List of workspaces the authenticated caller has access - """ - policies: Optional[List[AuthPolicy]] - """ - List of policies for the authenticated caller - """ - properties: Optional[Dict[str, str]] = Field(None, example={"key1": "value1"}) - """ - A map of key value properties - """ diff --git a/tests/integration/auth/test_auth_client.py b/tests/integration/auth/test_auth_client.py deleted file mode 100644 index f890f0cb..00000000 --- a/tests/integration/auth/test_auth_client.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Integration tests for AuthClient.""" - -import pytest -from nisystemlink.clients.auth import AuthClient - - -@pytest.fixture(scope="class") -def client(enterprise_config) -> AuthClient: - """Fixture to create a AuthClient instance.""" - return AuthClient(enterprise_config) - - -@pytest.mark.enterprise -@pytest.mark.integration -class TestAuthClient: - """Class contains a set of test methods to test SystemLink Auth API.""" - - def test__authenticate(self, client: AuthClient): - """Test the case of getting caller information with SystemLink Credentials.""" - response = client.authenticate() - assert response is not None From 77e9f1846186632f405bfdf10ffcab87613a2822 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Fri, 25 Oct 2024 13:52:24 +0530 Subject: [PATCH 23/44] fix: address some of the PR comments --- docs/getting_started.rst | 8 +-- examples/feeds/delete_feed.py | 2 +- examples/feeds/query_and_upload_feeds.py | 3 - nisystemlink/clients/feeds/_feeds_client.py | 18 +++-- nisystemlink/clients/feeds/models/__init__.py | 6 +- .../clients/feeds/models/_feeds_models.py | 65 +++++++++++++++++-- tests/integration/feeds/test_feeds_client.py | 6 +- 7 files changed, 78 insertions(+), 30 deletions(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index a29ed3f6..41a13fe0 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -190,7 +190,7 @@ Feeds API Overview ~~~~~~~~ -The :class:`.FeedsClient` class is the primary entry point of the File API. +The :class:`.FeedsClient` class is the primary entry point of the Feeds API. When constructing a :class:`.FeedsClient`, you can pass an :class:`.HttpConfiguration` (like one retrieved from the @@ -204,19 +204,19 @@ With a :class:`.FeedsClient` object, you can: Examples ~~~~~~~~ -Create new feed. +Create a new feed. .. literalinclude:: ../examples/feeds/create_feed.py :language: python :linenos: -Query feeds and Upload package to feed +Query feeds and upload a package to feed. .. literalinclude:: ../examples/feeds/query_and_upload_feeds.py :language: python :linenos: -Delete feed with its package +Delete a feed. .. literalinclude:: ../examples/feeds/delete_feed.py :language: python diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index d9b02a33..571efa9d 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -14,7 +14,7 @@ # Deleting Feed. try: - created_feed_name = client.delete_feed(feed_id=FEED_ID) + created_feed_name = client.delete_feed(id=FEED_ID) print("Feed deleted successfully.") except ApiException as exp: diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index d08e1eb8..c5346215 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -1,7 +1,5 @@ """Functionality of uploading & querying feeds APIs.""" -from typing import Dict - from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import Platform @@ -28,7 +26,6 @@ platform=PLATFORM, workspace=workspace_id, ) - existing_feeds: Dict[str, str] = {} feed_id = "" for feed in query_feeds.feeds: if feed.name == FEED_NAME and feed.id: diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 0169c629..ed6c88d5 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -11,7 +11,7 @@ class FeedsClient(BaseClient): - """Class contains a set of methods to access the APIs of SystemLink Feeds Client.""" + """A set of methods to access the APIs of SystemLink Feeds Client.""" def __init__(self, configuration: Optional[core.HttpConfiguration] = None): """Initialize an instance. @@ -31,9 +31,7 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None): super().__init__(configuration, base_path="/nifeed/v1/") @post("feeds") - def create_feed( - self, feed: models.CreateFeedRequest - ) -> models.CreateOrUpdateFeedResponse: + def create_feed(self, feed: models.CreateFeedRequest) -> models.Feed: """Create a new feed with the provided feed details. Args: @@ -49,7 +47,7 @@ def query_feeds( self, platform: Optional[str] = None, workspace: Optional[str] = None, - ) -> models.FeedsQueryResponse: + ) -> models.QueryFeedsResponse: """Get a set of feeds based on the provided `platform` and `workspace`. Args: @@ -57,7 +55,7 @@ def query_feeds( workspace (Optional[str]): Workspace id. Defaults to None. Returns: - models.FeedsQueryResponse: List of feeds. + models.QueryFeedsResponse: List of feeds. """ ... @@ -70,7 +68,7 @@ def upload_package( feed_id: str, package: Part, overwrite: Query = False, - ) -> models.UploadPackageResponse: + ) -> models.Package: """Upload package to SystemLink feed. Args: @@ -81,7 +79,7 @@ def upload_package( Defaults to False. Returns: - models.UploadPackageResponse: Uploaded package response information. + models.Package: Uploaded package response information. """ ... @@ -89,11 +87,11 @@ def upload_package( "feeds/{feedId}", args=[Path(name="feedId")], ) - def delete_feed(self, feed_id: str) -> None: + def delete_feed(self, id: str) -> None: """Delete feed and its packages. Args: - feed_id (str): ID of the feed to be deleted. + id (str): ID of the feed to be deleted. Returns: None. diff --git a/nisystemlink/clients/feeds/models/__init__.py b/nisystemlink/clients/feeds/models/__init__.py index 8ffcd563..841daa38 100644 --- a/nisystemlink/clients/feeds/models/__init__.py +++ b/nisystemlink/clients/feeds/models/__init__.py @@ -1,9 +1,9 @@ from ._feeds_models import ( CreateFeedRequest, - CreateOrUpdateFeedResponse, - FeedsQueryResponse, + Feed, + Package, Platform, - UploadPackageResponse, + QueryFeedsResponse, ) # flake8: noqa diff --git a/nisystemlink/clients/feeds/models/_feeds_models.py b/nisystemlink/clients/feeds/models/_feeds_models.py index 901c042b..d5ef760a 100644 --- a/nisystemlink/clients/feeds/models/_feeds_models.py +++ b/nisystemlink/clients/feeds/models/_feeds_models.py @@ -20,68 +20,121 @@ class CreateFeedRequest(JsonModel): """Create Feed Request.""" name: str + """The name of the feed.""" description: Optional[str] = None + """The description of the feed.""" platform: Platform + """The platform of the feed, the following package extensions are available: .nipkg for + windows feeds, .ipk and .deb for ni-linux-rt feeds.""" workspace: Optional[str] = None + """The ID of the workspace this feed belongs to. If the workspace is not defined, + the default workspace is used.""" -class CreateOrUpdateFeedResponse(JsonModel): - """Create or Update Feed Response.""" +class Feed(JsonModel): + """Feed model.""" id: Optional[str] = None + """The auto-generated ID of the feed.""" name: Optional[str] = None + """The name of the feed.""" description: Optional[str] = None + """The description of the feed.""" platform: Platform + """The platform of the feed, the following package extensions are available: .nipkg for + windows feeds, .ipk and .deb for ni-linux-rt feeds. + """ workspace: Optional[str] = None + """The ID of the workspace this feed belongs to.""" updated_at: str = Field(alias="updatedAt") + """The date of the latest feed update""" created_at: str = Field(alias="createdAt") + """The date when the feed was created at.""" package_sources: Optional[List[str]] = Field(default=None, alias="packageSources") + """The package sources list of the feed.""" deleted: bool + """Whether the feed deletion was requested.""" -class FeedsQueryResponse(JsonModel): +class QueryFeedsResponse(JsonModel): """Query Feeds response.""" - feeds: List[CreateOrUpdateFeedResponse] + feeds: List[Feed] + """A collection of feeds,""" class PackageMetadata(JsonModel): """Package Meta data.""" package_name: Optional[str] = Field(default=None, alias="packageName") + """The name of the package.""" version: Optional[str] = None + """The version number of the package.""" architecture: Optional[str] = None + """The architecture of the package.""" breaks: Optional[List[str]] = None + """Information about other packages this package breaks.""" conflicts: Optional[List[str]] = None + """Information about other packages this package conflicts with.""" depends: Optional[List[str]] = None + """Information about other packages this package depends on.""" description: Optional[str] = None + """The description of the package.""" enhances: Optional[List[str]] = None + """Information about other packages this package enchances.""" essential: Optional[bool] = None + """True if the package is essential.""" file_name: Optional[str] = Field(default=None, alias="fileName") + """The file name of the package. Depending on the selected platform, + the following package extensions are available: + .nipkg for windows feeds, .ipk and .deb for ni-linux-rt feeds.""" homepage: Optional[str] = None + """The website of the maintainers for the package.""" installed_size: Optional[int] = Field(default=None, alias="installedSize") + """The size of the package after install.""" maintainer: Optional[str] = None + """The maintainer of the package (name and email address).""" predepends: Optional[List[str]] = None + """Information about other packages this package predepends.""" priority: int + """The priority of the package.""" provides: Optional[List[str]] = None + """Information about other packages that this package provides.""" recommends: Optional[List[str]] = None + """Information about other packages this package recommends.""" release_notes: Optional[str] = Field(default=None, alias="releaseNotes") + """The release notes of the package.""" replaces: Optional[List[str]] = None + """Information about other packages this package replaces.""" section: Optional[str] = None + """The application area of the package.""" size: Optional[int] = None + """The size (in bytes) of the package.""" source: Optional[str] = None + """The source of the package.""" suggests: Optional[List[str]] = None + """Information about other packages this package suggests.""" tags: Optional[str] = None + """The tags of the package.""" attributes: Optional[Dict[str, str]] = None + """The attributes of the package.""" -class UploadPackageResponse(JsonModel): - """Upload package response.""" +class Package(JsonModel): + """Package model.""" id: Optional[str] = None + """Gets or sets the ID of this package. This is used to reference this package in the service.""" file_name: Optional[str] = Field(default=None, alias="fileName") + """The name of the file in this package.""" feed_id: Optional[str] = Field(default=None, alias="feedId") + """The ID of the feed this package is associated with.""" workspace: Optional[str] = None + """The ID of the workspace this package belongs to. + The workspace of a package is the workspace of feed this package is associated with.""" updated_at: str = Field(alias="updatedAt") + """The date of the latest package update.""" created_at: str = Field(alias="createdAt") + """The date when the package was created at.""" metadata: Optional[PackageMetadata] = None + """Package meta data.""" diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 0d9b4297..c3be1fa7 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -91,7 +91,7 @@ def get_linux_feed_id(client: FeedsClient): @pytest.mark.enterprise @pytest.mark.integration class TestFeedsClient: - """Class contains a set of test methods to test SystemLink Feeds API.""" + """A set of test methods to test SystemLink Feeds API.""" def test__create_feed_windows_platform( self, @@ -161,10 +161,10 @@ def test__upload_duplicate_package( def test__delete__windows_feed(self, client: FeedsClient, get_windows_feed_id: str): """Test the case of deleting windows feed with its packages.""" - response = client.delete_feed(feed_id=get_windows_feed_id) + response = client.delete_feed(id=get_windows_feed_id) assert response is None def test__delete__linux_feed(self, client: FeedsClient, get_linux_feed_id: str): """Test the case of deleting linux feed with its packages.""" - response = client.delete_feed(feed_id=get_linux_feed_id) + response = client.delete_feed(id=get_linux_feed_id) assert response is None From 570843eb8793b979dff6e658f26173efe4bed118 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 30 Oct 2024 15:29:21 +0530 Subject: [PATCH 24/44] fix: address rest of the PR comments --- examples/feeds/create_feed.py | 2 +- examples/feeds/query_and_upload_feeds.py | 28 ++- nisystemlink/clients/feeds/_feeds_client.py | 55 ++++- nisystemlink/clients/feeds/utilities.py | 24 +++ tests/integration/feeds/test_feeds_client.py | 205 ++++++++++++++----- 5 files changed, 238 insertions(+), 76 deletions(-) create mode 100644 nisystemlink/clients/feeds/utilities.py diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index 6a2ad561..81a2cd21 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -10,7 +10,7 @@ FEED_NAME = "EXAMPLE FEED" FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" -PLATFORM = Platform.WINDOWS.value +PLATFORM = Platform.WINDOWS server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index c5346215..aae7c5fc 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -3,12 +3,12 @@ from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import Platform - +from nisystemlink.clients.feeds.utilities import get_feed_id # Constant FEED_NAME = "EXAMPLE FEED" FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" -PLATFORM = Platform.WINDOWS.value +PLATFORM = Platform.WINDOWS PACKAGE_NAME = "" PACKAGE_PATH = "" @@ -22,22 +22,20 @@ # To upload a package to feed. try: # To query available feeds. - query_feeds = client.query_feeds( + query_feeds_response = client.query_feeds( platform=PLATFORM, workspace=workspace_id, ) - feed_id = "" - for feed in query_feeds.feeds: - if feed.name == FEED_NAME and feed.id: - feed_id = feed.id - break - - upload_package = client.upload_package( - feed_id=feed_id, - overwrite=True, - package=(PACKAGE_NAME, open(PACKAGE_PATH, "rb"), "multipart/form-data"), - ) - print("Package uploaded sucessfully.") + + feed_id = get_feed_id(feeds_details=query_feeds_response.feeds, feed_name=FEED_NAME) + + if feed_id: + upload_package = client.upload_package( + feed_id=feed_id, + overwrite=True, + package=open(PACKAGE_PATH, "rb"), + ) + print("Package uploaded sucessfully.") except ApiException as exp: print(exp) diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index ed6c88d5..b4d03744 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -1,6 +1,6 @@ """Implementation of SystemLink Feeds Client.""" -from typing import Optional +from typing import BinaryIO, Optional from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient @@ -43,7 +43,7 @@ def create_feed(self, feed: models.CreateFeedRequest) -> models.Feed: ... @get("feeds", args=[Query, Query]) - def query_feeds( + def __query_feeds( self, platform: Optional[str] = None, workspace: Optional[str] = None, @@ -59,11 +59,34 @@ def query_feeds( """ ... + def query_feeds( + self, + platform: Optional[models.Platform] = None, + workspace: Optional[str] = None, + ) -> models.QueryFeedsResponse: + """Get a set of feeds based on the provided `platform` and `workspace`. + + Args: + platform (Optional[models.Platform], optional): Information about system platform. \ +Defaults to None. + workspace (Optional[str], optional): Workspace id. Defaults to None. + + Returns: + models.QueryFeedsResponse: List of feeds. + """ + platform_by_str = platform.value if platform is not None else None + response = self.__query_feeds( + platform=platform_by_str, + workspace=workspace, + ) + + return response + @post( "feeds/{feedId}/packages", args=[Path(name="feedId"), Query(name="ShouldOverwrite")], ) - def upload_package( + def __upload_package( self, feed_id: str, package: Part, @@ -74,7 +97,6 @@ def upload_package( Args: feed_id (str): ID of the feed. package (Part): Package file as a form data. - Example: `package=open(filename, "rb")` overwrite (Query): Set to True to overwrite the package if it already exists.\ Defaults to False. @@ -83,6 +105,31 @@ def upload_package( """ ... + def upload_package( + self, + feed_id: str, + package: BinaryIO, + overwrite: bool = False, + ) -> models.Package: + """Upload package to SystemLink feed. + + Args: + feed_id (str): ID of the feed. + package (BinaryIO): Package file to be uploaded. + overwrite (bool): Set to True to overwrite the package if it already exists.\ +Defaults to False. + + Returns: + models.Package: Uploaded package response information. + """ + response = self.__upload_package( + feed_id=feed_id, + overwrite=overwrite, + package=package, + ) + + return response + @delete( "feeds/{feedId}", args=[Path(name="feedId")], diff --git a/nisystemlink/clients/feeds/utilities.py b/nisystemlink/clients/feeds/utilities.py new file mode 100644 index 00000000..f483155d --- /dev/null +++ b/nisystemlink/clients/feeds/utilities.py @@ -0,0 +1,24 @@ +"""Utilities for FeedsClient.""" + +from typing import List, Union + +from nisystemlink.clients.feeds.models import Feed + + +def get_feed_id( + feeds_details: List[Feed], + feed_name: str, +) -> Union[str, None]: + """Get feed id from the list of feed details using `feed_name`. + + Args: + feeds_details (List[Feed]): List of feed details. + feed_name (str): Feed name. + + Returns: + Union[str, None]: Feed ID of the `feed_name`. + """ + for feed in feeds_details: + if feed.name == feed_name and feed.id: + return feed.id + return None diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index c3be1fa7..558ca0bc 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -1,7 +1,8 @@ """Integration tests for FeedsClient.""" from pathlib import Path -from typing import Callable +from random import randint +from typing import BinaryIO, Callable import pytest from nisystemlink.clients.core import ApiException @@ -9,10 +10,7 @@ from nisystemlink.clients.feeds.models import CreateFeedRequest, Platform -WINDOWS_FEED_NAME = "Windows feed" -LINUX_FEED_NAME = "Linux feed" FEED_DESCRIPTION = "Sample feed for uploading packages" -INVALID_WORKSPACE_ID = "12345" PACKAGE_PATH = ( Path(__file__).parent.resolve() / "test_files" @@ -30,23 +28,29 @@ def client(enterprise_config) -> FeedsClient: @pytest.fixture(scope="class") def create_feed(client: FeedsClient): """Fixture to return a object that creates feed.""" + feed_ids = [] def _create_feed(feed): response = client.create_feed(feed) + feed_ids.append(response.id) return response yield _create_feed + # deleting the created feeds. + for feed_id in feed_ids: + client.delete_feed(id=feed_id) + @pytest.fixture(scope="class") def create_windows_feed_request(): """Fixture to create a request body of create feed API for windows platform.""" - def _create_feed_request(): + def _create_feed_request(feed_name: str, description: str, platform: Platform): feed_request = CreateFeedRequest( - name=WINDOWS_FEED_NAME, - platform=Platform.WINDOWS.value, - description=FEED_DESCRIPTION, + name=feed_name, + platform=platform, + description=description, ) return feed_request @@ -57,11 +61,11 @@ def _create_feed_request(): def create_linux_feed_request(): """Fixture to create a request body of create feed API for linux platform.""" - def _create_feed_request(): + def _create_feed_request(feed_name: str, description: str, platform: Platform): feed_request = CreateFeedRequest( - name=LINUX_FEED_NAME, - platform=Platform.NI_LINUX_RT.value, - description=FEED_DESCRIPTION, + name=feed_name, + platform=platform, + description=description, ) return feed_request @@ -69,23 +73,32 @@ def _create_feed_request(): @pytest.fixture(scope="class") -def get_windows_feed_id(client: FeedsClient): - """Fixture to return the Feed ID of the created windows feed.""" - query_feeds_response = client.query_feeds(platform=Platform.WINDOWS.value) +def binary_pkg_file_data() -> BinaryIO: + """Fixture to return Binary package file data.""" + package_data = open(PACKAGE_PATH, "rb") + return package_data - for feed in query_feeds_response.feeds: - if feed.name == WINDOWS_FEED_NAME: - return feed.id + +@pytest.fixture(scope="class") +def invalid_id() -> str: + """Generate a invalid id.""" + id = f"Invalid-id-{randint(1000, 9999)}" + return id @pytest.fixture(scope="class") -def get_linux_feed_id(client: FeedsClient): - """Fixture to return the Feed ID of the created Linux feed.""" - query_feeds_response = client.query_feeds(platform=Platform.NI_LINUX_RT.value) +def get_feed_name(): + """Generate a feed name.""" + name = "Test_Feeds_" + feed_count = 0 + + def _get_feed_name(): + nonlocal feed_count + feed_count += 1 + feed_name = name + str(feed_count) + return feed_name - for feed in query_feeds_response.feeds: - if feed.name == LINUX_FEED_NAME: - return feed.id + yield _get_feed_name @pytest.mark.enterprise @@ -93,78 +106,158 @@ def get_linux_feed_id(client: FeedsClient): class TestFeedsClient: """A set of test methods to test SystemLink Feeds API.""" - def test__create_feed_windows_platform( + def test__create_feed_windows_platform__succeeds( self, create_feed: Callable, create_windows_feed_request: Callable, + get_feed_name: Callable, ): """Test the case of a completely successful create feed API for windows platform.""" - request_body = create_windows_feed_request() + request_body = create_windows_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.WINDOWS, + ) response = create_feed(request_body) assert response.id is not None assert response.workspace is not None - assert response.name == WINDOWS_FEED_NAME + assert response.name is not None assert response.platform == Platform.WINDOWS assert response.description == FEED_DESCRIPTION - def test__create_feed_linux_platform( + def test__create_feed_linux_platform__succeeds( self, create_feed: Callable, create_linux_feed_request: Callable, + get_feed_name: Callable, ): """Test the case of a completely successful create feed API for Linux platform.""" - request_body = create_linux_feed_request() + request_body = create_linux_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.NI_LINUX_RT, + ) response = create_feed(request_body) assert response.id is not None assert response.workspace is not None - assert response.name == LINUX_FEED_NAME + assert response.name is not None assert response.platform == Platform.NI_LINUX_RT assert response.description == FEED_DESCRIPTION - def test__query_feeds_windows_platform(self, client: FeedsClient): + def test__query_feeds_windows_platform__succeeds( + self, + client: FeedsClient, + create_feed: Callable, + create_windows_feed_request: Callable, + get_feed_name: Callable, + ): """Test the case for querying available feeds for windows platform.""" - response = client.query_feeds(platform=Platform.WINDOWS.value) - assert response is not None + create_feed_request_body = create_windows_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.WINDOWS, + ) + create_feed(create_feed_request_body) + + query_feed_resp = client.query_feeds(platform=Platform.WINDOWS) + assert query_feed_resp is not None + + def test__query_feeds_linux_platform__succeeds( + self, + client: FeedsClient, + create_feed: Callable, + create_linux_feed_request: Callable, + get_feed_name: Callable, + ): + """Test the case for querying available feeds for windows platform.""" + create_feed_request_body = create_linux_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.NI_LINUX_RT, + ) + create_feed(create_feed_request_body) - def test__query_feeds_linux_platform(self, client: FeedsClient): - """Test the case for querying available feeds for linux platform.""" - response = client.query_feeds(platform=Platform.NI_LINUX_RT.value) - assert response is not None + query_feed_resp = client.query_feeds(platform=Platform.NI_LINUX_RT) + assert query_feed_resp is not None - def test__query_feeds_invalid_workspace(self, client: FeedsClient): + def test__query_feeds__invalid_workspace_raises( + self, + client: FeedsClient, + invalid_id: str, + ): """Test the case of query feeds API with invalid workspace id.""" with pytest.raises(ApiException, match="UnauthorizedWorkspaceError"): - client.query_feeds(workspace=INVALID_WORKSPACE_ID) + client.query_feeds(workspace=invalid_id) - def test__upload_package(self, client: FeedsClient, get_windows_feed_id: str): + def test__upload_package__succeeds( + self, + client: FeedsClient, + create_feed: Callable, + create_windows_feed_request: Callable, + binary_pkg_file_data: BinaryIO, + get_feed_name: Callable, + ): """Test the case of upload package to feed.""" - response = client.upload_package( - package=open(PACKAGE_PATH, "rb"), - feed_id=get_windows_feed_id, + create_feed_request_body = create_windows_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.WINDOWS, + ) + create_feed_resp = create_feed(create_feed_request_body) + + upload_pacakge_rsp = client.upload_package( + package=binary_pkg_file_data, + feed_id=create_feed_resp.id, overwrite=True, ) - assert response is not None + assert upload_pacakge_rsp is not None - def test__upload_duplicate_package( + def test__upload_package__invalid_feed_id_raises( self, client: FeedsClient, - get_windows_feed_id: str, + binary_pkg_file_data: BinaryIO, + invalid_id: str, ): - """Test the case of uploading duplicate package to feed.""" - with pytest.raises(ApiException, match="DuplicatePackageError"): + """Test the case of uploading package to Invalid feed.""" + with pytest.raises(ApiException, match="FeedNotFoundError"): client.upload_package( - package=open(PACKAGE_PATH, "rb"), - feed_id=get_windows_feed_id, + package=binary_pkg_file_data, + feed_id=invalid_id, ) - def test__delete__windows_feed(self, client: FeedsClient, get_windows_feed_id: str): + def test__delete_windows_feed__succeeds( + self, + client: FeedsClient, + create_feed: Callable, + create_windows_feed_request: Callable, + get_feed_name: Callable, + ): """Test the case of deleting windows feed with its packages.""" - response = client.delete_feed(id=get_windows_feed_id) - assert response is None + create_feed_request_body = create_windows_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.WINDOWS, + ) + create_feed_resp = create_feed(create_feed_request_body) + + delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None - def test__delete__linux_feed(self, client: FeedsClient, get_linux_feed_id: str): + def test__delete__linux_feed__succeeds( + self, + client: FeedsClient, + create_feed: Callable, + create_windows_feed_request: Callable, + get_feed_name: Callable, + ): """Test the case of deleting linux feed with its packages.""" - response = client.delete_feed(id=get_linux_feed_id) - assert response is None + create_feed_request_body = create_windows_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.NI_LINUX_RT, + ) + create_feed_resp = create_feed(create_feed_request_body) + delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None From 7d653121a166dfb253b768805fb06f8301997bf9 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 30 Oct 2024 18:47:21 +0530 Subject: [PATCH 25/44] fix: address self review comments --- nisystemlink/clients/feeds/_feeds_client.py | 10 ++-- .../clients/feeds/models/_feeds_models.py | 2 +- tests/integration/feeds/test_feeds_client.py | 59 +++++++------------ 3 files changed, 28 insertions(+), 43 deletions(-) diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index b4d03744..3a48f8bf 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -38,7 +38,7 @@ def create_feed(self, feed: models.CreateFeedRequest) -> models.Feed: feeds (models.CreateFeedsRequest): Request model to create the feed. Returns: - models.CreateorUpdateFeedsResponse: Details of the created feed. + models.Feed: Details of the created feed. """ ... @@ -67,9 +67,9 @@ def query_feeds( """Get a set of feeds based on the provided `platform` and `workspace`. Args: - platform (Optional[models.Platform], optional): Information about system platform. \ + platform (Optional[models.Platform]): Information about system platform. \ Defaults to None. - workspace (Optional[str], optional): Workspace id. Defaults to None. + workspace (Optional[str]): Workspace id. Defaults to None. Returns: models.QueryFeedsResponse: List of feeds. @@ -97,7 +97,7 @@ def __upload_package( Args: feed_id (str): ID of the feed. package (Part): Package file as a form data. - overwrite (Query): Set to True to overwrite the package if it already exists.\ + overwrite (Query): Set to True, to overwrite the package if it already exists.\ Defaults to False. Returns: @@ -116,7 +116,7 @@ def upload_package( Args: feed_id (str): ID of the feed. package (BinaryIO): Package file to be uploaded. - overwrite (bool): Set to True to overwrite the package if it already exists.\ + overwrite (bool): Set to True, to overwrite the package if it already exists.\ Defaults to False. Returns: diff --git a/nisystemlink/clients/feeds/models/_feeds_models.py b/nisystemlink/clients/feeds/models/_feeds_models.py index d5ef760a..3fbc6280 100644 --- a/nisystemlink/clients/feeds/models/_feeds_models.py +++ b/nisystemlink/clients/feeds/models/_feeds_models.py @@ -60,7 +60,7 @@ class QueryFeedsResponse(JsonModel): """Query Feeds response.""" feeds: List[Feed] - """A collection of feeds,""" + """A collection of feeds""" class PackageMetadata(JsonModel): diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 558ca0bc..9190f108 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -43,23 +43,8 @@ def _create_feed(feed): @pytest.fixture(scope="class") -def create_windows_feed_request(): - """Fixture to create a request body of create feed API for windows platform.""" - - def _create_feed_request(feed_name: str, description: str, platform: Platform): - feed_request = CreateFeedRequest( - name=feed_name, - platform=platform, - description=description, - ) - return feed_request - - yield _create_feed_request - - -@pytest.fixture(scope="class") -def create_linux_feed_request(): - """Fixture to create a request body of create feed API for linux platform.""" +def create_feed_request(): + """Fixture to create a request body of create feed API.""" def _create_feed_request(feed_name: str, description: str, platform: Platform): feed_request = CreateFeedRequest( @@ -74,7 +59,7 @@ def _create_feed_request(feed_name: str, description: str, platform: Platform): @pytest.fixture(scope="class") def binary_pkg_file_data() -> BinaryIO: - """Fixture to return Binary package file data.""" + """Fixture to return package file in binary format.""" package_data = open(PACKAGE_PATH, "rb") return package_data @@ -109,11 +94,11 @@ class TestFeedsClient: def test__create_feed_windows_platform__succeeds( self, create_feed: Callable, - create_windows_feed_request: Callable, + create_feed_request: Callable, get_feed_name: Callable, ): - """Test the case of a completely successful create feed API for windows platform.""" - request_body = create_windows_feed_request( + """Test the case of a completely successful create feed API for Windows platform.""" + request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.WINDOWS, @@ -129,11 +114,11 @@ def test__create_feed_windows_platform__succeeds( def test__create_feed_linux_platform__succeeds( self, create_feed: Callable, - create_linux_feed_request: Callable, + create_feed_request: Callable, get_feed_name: Callable, ): """Test the case of a completely successful create feed API for Linux platform.""" - request_body = create_linux_feed_request( + request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.NI_LINUX_RT, @@ -150,11 +135,11 @@ def test__query_feeds_windows_platform__succeeds( self, client: FeedsClient, create_feed: Callable, - create_windows_feed_request: Callable, + create_feed_request: Callable, get_feed_name: Callable, ): - """Test the case for querying available feeds for windows platform.""" - create_feed_request_body = create_windows_feed_request( + """Test the case for querying available feeds for Windows platform.""" + create_feed_request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.WINDOWS, @@ -168,11 +153,11 @@ def test__query_feeds_linux_platform__succeeds( self, client: FeedsClient, create_feed: Callable, - create_linux_feed_request: Callable, + create_feed_request: Callable, get_feed_name: Callable, ): - """Test the case for querying available feeds for windows platform.""" - create_feed_request_body = create_linux_feed_request( + """Test the case for querying available feeds for Linux platform.""" + create_feed_request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.NI_LINUX_RT, @@ -195,12 +180,12 @@ def test__upload_package__succeeds( self, client: FeedsClient, create_feed: Callable, - create_windows_feed_request: Callable, + create_feed_request: Callable, binary_pkg_file_data: BinaryIO, get_feed_name: Callable, ): """Test the case of upload package to feed.""" - create_feed_request_body = create_windows_feed_request( + create_feed_request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.WINDOWS, @@ -231,11 +216,11 @@ def test__delete_windows_feed__succeeds( self, client: FeedsClient, create_feed: Callable, - create_windows_feed_request: Callable, + create_feed_request: Callable, get_feed_name: Callable, ): - """Test the case of deleting windows feed with its packages.""" - create_feed_request_body = create_windows_feed_request( + """Test the case of deleting Windows feed.""" + create_feed_request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.WINDOWS, @@ -249,11 +234,11 @@ def test__delete__linux_feed__succeeds( self, client: FeedsClient, create_feed: Callable, - create_windows_feed_request: Callable, + create_feed_request: Callable, get_feed_name: Callable, ): - """Test the case of deleting linux feed with its packages.""" - create_feed_request_body = create_windows_feed_request( + """Test the case of deleting Linux feed.""" + create_feed_request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, platform=Platform.NI_LINUX_RT, From 3ea05212eaa4394d0b88e1b0bb5fbc71f27fd5e0 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 30 Oct 2024 19:41:14 +0530 Subject: [PATCH 26/44] fix: error due to del feed with invalid id --- tests/integration/feeds/test_feeds_client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 9190f108..a59c72fb 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -215,7 +215,6 @@ def test__upload_package__invalid_feed_id_raises( def test__delete_windows_feed__succeeds( self, client: FeedsClient, - create_feed: Callable, create_feed_request: Callable, get_feed_name: Callable, ): @@ -225,15 +224,14 @@ def test__delete_windows_feed__succeeds( description=FEED_DESCRIPTION, platform=Platform.WINDOWS, ) - create_feed_resp = create_feed(create_feed_request_body) - + create_feed_resp = client.create_feed(create_feed_request_body) delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None def test__delete__linux_feed__succeeds( self, client: FeedsClient, - create_feed: Callable, create_feed_request: Callable, get_feed_name: Callable, ): @@ -243,6 +241,7 @@ def test__delete__linux_feed__succeeds( description=FEED_DESCRIPTION, platform=Platform.NI_LINUX_RT, ) - create_feed_resp = create_feed(create_feed_request_body) + create_feed_resp = client.create_feed(create_feed_request_body) delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None From 99320bab5f301077e0955abcc87cdd933cce4e86 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Wed, 30 Oct 2024 19:46:01 +0530 Subject: [PATCH 27/44] fix: mypy issues due to incompatible types --- tests/integration/feeds/test_feeds_client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index a59c72fb..b8d44e5a 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -225,9 +225,10 @@ def test__delete_windows_feed__succeeds( platform=Platform.WINDOWS, ) create_feed_resp = client.create_feed(create_feed_request_body) - delete_feed_resp = client.delete_feed(id=create_feed_resp.id) - assert delete_feed_resp is None + if create_feed_resp.id: + delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None def test__delete__linux_feed__succeeds( self, @@ -242,6 +243,7 @@ def test__delete__linux_feed__succeeds( platform=Platform.NI_LINUX_RT, ) create_feed_resp = client.create_feed(create_feed_request_body) - delete_feed_resp = client.delete_feed(id=create_feed_resp.id) - assert delete_feed_resp is None + if create_feed_resp.id: + delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None From 8efc6de28c416368c55ced8fa5f2e9934c0268b6 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Sat, 2 Nov 2024 11:27:43 +0530 Subject: [PATCH 28/44] fix: improvement PR comments --- examples/feeds/create_feed.py | 4 +-- examples/feeds/delete_feed.py | 17 ++++++++-- examples/feeds/query_and_upload_feeds.py | 20 +++++------ nisystemlink/clients/feeds/_feeds_client.py | 35 +++++++++++++++++--- nisystemlink/clients/feeds/utilities.py | 20 +++++------ tests/integration/feeds/test_feeds_client.py | 28 ++++++++++++++-- 6 files changed, 90 insertions(+), 34 deletions(-) diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index 81a2cd21..8ebeba14 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -27,10 +27,10 @@ platform=PLATFORM, workspace=workspace_id, ) - created_feed_name = client.create_feed(feed=feed_request).name + feed_details = client.create_feed(feed=feed_request) print("Feed created Successfully.") - print(f"Created feed name: {created_feed_name}") + print(f"Created feed details: {feed_details}") except ApiException as exp: print(exp) diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index 571efa9d..23d94c72 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -2,20 +2,31 @@ from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient +from nisystemlink.clients.feeds.models import Platform +from nisystemlink.clients.feeds.utilities import get_feed_by_name -FEED_ID = "" +FEED_NAME = "EXAMPLE FEED" # Name of the feed. +PLATFORM = Platform.WINDOWS server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key +workspace_id = ( + None # None uses Default workspace. Replace with Systemlink workspace id. +) # Please provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # Deleting Feed. try: - created_feed_name = client.delete_feed(id=FEED_ID) - print("Feed deleted successfully.") + # To query available feeds. + query_feeds_response = client.query_feeds(platform=PLATFORM, workspace=workspace_id) + feed_details = get_feed_by_name(feeds=query_feeds_response, name=FEED_NAME) + + if feed_details and feed_details.id: + created_feed_name = client.delete_feed(id=feed_details.id) + print("Feed deleted successfully.") except ApiException as exp: print(exp) diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index aae7c5fc..438c55e7 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -3,7 +3,7 @@ from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import Platform -from nisystemlink.clients.feeds.utilities import get_feed_id +from nisystemlink.clients.feeds.utilities import get_feed_by_name # Constant FEED_NAME = "EXAMPLE FEED" @@ -14,7 +14,9 @@ server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -workspace_id = "" # Systemlink workspace id +workspace_id = ( + None # None uses Default workspace. Replace with Systemlink workspace id. +) # Please provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) @@ -22,18 +24,14 @@ # To upload a package to feed. try: # To query available feeds. - query_feeds_response = client.query_feeds( - platform=PLATFORM, - workspace=workspace_id, - ) + query_feeds_response = client.query_feeds(platform=PLATFORM, workspace=workspace_id) + feed_details = get_feed_by_name(feeds=query_feeds_response, name=FEED_NAME) - feed_id = get_feed_id(feeds_details=query_feeds_response.feeds, feed_name=FEED_NAME) - - if feed_id: + if feed_details and feed_details.id: upload_package = client.upload_package( - feed_id=feed_id, + feed_id=feed_details.id, overwrite=True, - package=open(PACKAGE_PATH, "rb"), + package_file_path=PACKAGE_PATH, ) print("Package uploaded sucessfully.") diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 3a48f8bf..54fc9acb 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -1,6 +1,6 @@ """Implementation of SystemLink Feeds Client.""" -from typing import BinaryIO, Optional +from typing import BinaryIO, List, Optional from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient @@ -63,7 +63,7 @@ def query_feeds( self, platform: Optional[models.Platform] = None, workspace: Optional[str] = None, - ) -> models.QueryFeedsResponse: + ) -> List[models.Feed]: """Get a set of feeds based on the provided `platform` and `workspace`. Args: @@ -72,13 +72,13 @@ def query_feeds( workspace (Optional[str]): Workspace id. Defaults to None. Returns: - models.QueryFeedsResponse: List of feeds. + List[models.Feed]: List of feeds. """ platform_by_str = platform.value if platform is not None else None response = self.__query_feeds( platform=platform_by_str, workspace=workspace, - ) + ).feeds return response @@ -106,6 +106,31 @@ def __upload_package( ... def upload_package( + self, + feed_id: str, + package_file_path: str, + overwrite: bool = False, + ) -> models.Package: + """Upload package to SystemLink feed. + + Args: + feed_id (str): ID of the feed. + package_file_path (str): File path of the package to be uploaded. + overwrite (bool): Set to True, to overwrite the package if it already exists.\ +Defaults to False. + + Returns: + models.Package: Uploaded package information. + """ + response = self.__upload_package( + feed_id=feed_id, + overwrite=overwrite, + package=open(package_file_path, "rb"), + ) + + return response + + def upload_package_content( self, feed_id: str, package: BinaryIO, @@ -120,7 +145,7 @@ def upload_package( Defaults to False. Returns: - models.Package: Uploaded package response information. + models.Package: Uploaded package information. """ response = self.__upload_package( feed_id=feed_id, diff --git a/nisystemlink/clients/feeds/utilities.py b/nisystemlink/clients/feeds/utilities.py index f483155d..12e1a0bb 100644 --- a/nisystemlink/clients/feeds/utilities.py +++ b/nisystemlink/clients/feeds/utilities.py @@ -1,24 +1,24 @@ """Utilities for FeedsClient.""" -from typing import List, Union +from typing import List, Optional from nisystemlink.clients.feeds.models import Feed -def get_feed_id( - feeds_details: List[Feed], - feed_name: str, -) -> Union[str, None]: +def get_feed_by_name( + feeds: List[Feed], + name: str, +) -> Optional[Feed]: """Get feed id from the list of feed details using `feed_name`. Args: - feeds_details (List[Feed]): List of feed details. + feeds (List[Feed]): List of feed details. feed_name (str): Feed name. Returns: - Union[str, None]: Feed ID of the `feed_name`. + Optional[Feed]: Feed information. """ - for feed in feeds_details: - if feed.name == feed_name and feed.id: - return feed.id + for feed in feeds: + if feed.name == name and feed.id: + return feed return None diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index b8d44e5a..c8dcadfd 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -181,7 +181,6 @@ def test__upload_package__succeeds( client: FeedsClient, create_feed: Callable, create_feed_request: Callable, - binary_pkg_file_data: BinaryIO, get_feed_name: Callable, ): """Test the case of upload package to feed.""" @@ -193,13 +192,36 @@ def test__upload_package__succeeds( create_feed_resp = create_feed(create_feed_request_body) upload_pacakge_rsp = client.upload_package( + package_file_path=PACKAGE_PATH, + feed_id=create_feed_resp.id, + overwrite=True, + ) + assert upload_pacakge_rsp is not None + + def test__upload_package_content__succeeds( + self, + client: FeedsClient, + create_feed: Callable, + create_feed_request: Callable, + binary_pkg_file_data: BinaryIO, + get_feed_name: Callable, + ): + """Test the case of upload package to feed.""" + create_feed_request_body = create_feed_request( + feed_name=get_feed_name(), + description=FEED_DESCRIPTION, + platform=Platform.WINDOWS, + ) + create_feed_resp = create_feed(create_feed_request_body) + + upload_pacakge_rsp = client.upload_package_content( package=binary_pkg_file_data, feed_id=create_feed_resp.id, overwrite=True, ) assert upload_pacakge_rsp is not None - def test__upload_package__invalid_feed_id_raises( + def test__upload_package_content__invalid_feed_id_raises( self, client: FeedsClient, binary_pkg_file_data: BinaryIO, @@ -207,7 +229,7 @@ def test__upload_package__invalid_feed_id_raises( ): """Test the case of uploading package to Invalid feed.""" with pytest.raises(ApiException, match="FeedNotFoundError"): - client.upload_package( + client.upload_package_content( package=binary_pkg_file_data, feed_id=invalid_id, ) From cb30e6dc95563b17775bcb00a1e7522e1790d778 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Sat, 2 Nov 2024 11:32:53 +0530 Subject: [PATCH 29/44] fix: mypy issue due to var type incompatibility --- tests/integration/feeds/test_feeds_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index c8dcadfd..a689e716 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -11,12 +11,11 @@ FEED_DESCRIPTION = "Sample feed for uploading packages" -PACKAGE_PATH = ( +PACKAGE_PATH = str( Path(__file__).parent.resolve() / "test_files" / "sample-measurement_0.5.0_windows_x64.nipkg" ) -INVALID_PACKAGE_PATH = Path(__file__).parent.resolve() @pytest.fixture(scope="class") From 8edf916438e80e9a14a93362968a0a59d7a4be2c Mon Sep 17 00:00:00 2001 From: Giriharan Date: Mon, 4 Nov 2024 10:38:33 +0530 Subject: [PATCH 30/44] fix: update self review comments --- examples/feeds/create_feed.py | 5 ++++- examples/feeds/delete_feed.py | 3 ++- examples/feeds/query_and_upload_feeds.py | 3 ++- tests/integration/feeds/test_feeds_client.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index 8ebeba14..40498672 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -14,7 +14,10 @@ server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -workspace_id = "" # Systemlink workspace id +workspace_id = ( + None # None uses Default workspace. Replace with Systemlink workspace id. +) + # Please provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index 23d94c72..6b52c090 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -15,6 +15,7 @@ None # None uses Default workspace. Replace with Systemlink workspace id. ) + # Please provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) @@ -25,7 +26,7 @@ feed_details = get_feed_by_name(feeds=query_feeds_response, name=FEED_NAME) if feed_details and feed_details.id: - created_feed_name = client.delete_feed(id=feed_details.id) + client.delete_feed(id=feed_details.id) print("Feed deleted successfully.") except ApiException as exp: diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index 438c55e7..a8952871 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -18,6 +18,7 @@ None # None uses Default workspace. Replace with Systemlink workspace id. ) + # Please provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) @@ -28,7 +29,7 @@ feed_details = get_feed_by_name(feeds=query_feeds_response, name=FEED_NAME) if feed_details and feed_details.id: - upload_package = client.upload_package( + client.upload_package( feed_id=feed_details.id, overwrite=True, package_file_path=PACKAGE_PATH, diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index a689e716..2e55e691 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -205,7 +205,7 @@ def test__upload_package_content__succeeds( binary_pkg_file_data: BinaryIO, get_feed_name: Callable, ): - """Test the case of upload package to feed.""" + """Test the case of upload package content to feed.""" create_feed_request_body = create_feed_request( feed_name=get_feed_name(), description=FEED_DESCRIPTION, From 552c39d8c86d8a824eb1201bc10d7b457dca8231 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Mon, 4 Nov 2024 14:13:59 +0530 Subject: [PATCH 31/44] fix: issue due to pipeline failure --- tests/integration/feeds/test_feeds_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 2e55e691..f0b64a51 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -73,7 +73,7 @@ def invalid_id() -> str: @pytest.fixture(scope="class") def get_feed_name(): """Generate a feed name.""" - name = "Test_Feeds_" + name = "Test_Feed_" feed_count = 0 def _get_feed_name(): From 6dfb17e5b82673cfb7ad73ace5625de4d2e4c4ec Mon Sep 17 00:00:00 2001 From: Giriharan Date: Mon, 4 Nov 2024 18:27:54 +0530 Subject: [PATCH 32/44] docs: update the doc with new api for upload feed --- docs/api_reference/feeds.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api_reference/feeds.rst b/docs/api_reference/feeds.rst index 739045f9..9bc2e27a 100644 --- a/docs/api_reference/feeds.rst +++ b/docs/api_reference/feeds.rst @@ -10,6 +10,7 @@ nisystemlink.clients.feeds .. automethod:: create_feed .. automethod:: query_feeds .. automethod:: upload_package + .. automethod:: upload_package_content .. automethod:: delete_feed .. automodule:: nisystemlink.clients.feeds.models From 05e9dcc205c9ee58dad623be14df4e859103089d Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 10:43:50 +0530 Subject: [PATCH 33/44] fix: address another set of review comments --- examples/feeds/delete_feed.py | 14 ++-- examples/feeds/query_and_upload_feeds.py | 12 +-- nisystemlink/clients/feeds/_feeds_client.py | 29 ++++++-- nisystemlink/clients/feeds/models/__init__.py | 12 ++- .../clients/feeds/models/_create_feeds.py | 22 ++++++ nisystemlink/clients/feeds/models/_feed.py | 32 ++++++++ .../models/{_feeds_models.py => _package.py} | 74 ++----------------- .../clients/feeds/models/_platform.py | 10 +++ .../clients/feeds/models/_query_feeds.py | 14 ++++ nisystemlink/clients/feeds/utilities.py | 4 +- tests/integration/feeds/test_feeds_client.py | 34 +++++---- 11 files changed, 151 insertions(+), 106 deletions(-) create mode 100644 nisystemlink/clients/feeds/models/_create_feeds.py create mode 100644 nisystemlink/clients/feeds/models/_feed.py rename nisystemlink/clients/feeds/models/{_feeds_models.py => _package.py} (57%) create mode 100644 nisystemlink/clients/feeds/models/_platform.py create mode 100644 nisystemlink/clients/feeds/models/_query_feeds.py diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index 6b52c090..75624353 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -21,12 +21,14 @@ # Deleting Feed. try: - # To query available feeds. - query_feeds_response = client.query_feeds(platform=PLATFORM, workspace=workspace_id) - feed_details = get_feed_by_name(feeds=query_feeds_response, name=FEED_NAME) - - if feed_details and feed_details.id: - client.delete_feed(id=feed_details.id) + # Get ID of the Feed to delete by name + feeds = client.query_feeds(platform=PLATFORM, workspace=workspace_id) + feed = get_feed_by_name(feeds=feeds, name=FEED_NAME) + feed_id = feed.id if feed else None + + # Delete the Feed by ID + if feed_id: + client.delete_feed(id=feed_id) print("Feed deleted successfully.") except ApiException as exp: diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index a8952871..5ad7d072 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -24,13 +24,15 @@ # To upload a package to feed. try: - # To query available feeds. - query_feeds_response = client.query_feeds(platform=PLATFORM, workspace=workspace_id) - feed_details = get_feed_by_name(feeds=query_feeds_response, name=FEED_NAME) + # Get ID of the Feed to delete by name + feeds = client.query_feeds(platform=PLATFORM, workspace=workspace_id) + feed = get_feed_by_name(feeds=feeds, name=FEED_NAME) + feed_id = feed.id if feed else None - if feed_details and feed_details.id: + # Upload the package to Feed by ID + if feed_id: client.upload_package( - feed_id=feed_details.id, + feed_id=feed_id, overwrite=True, package_file_path=PACKAGE_PATH, ) diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 54fc9acb..4d7f11c2 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -11,8 +11,6 @@ class FeedsClient(BaseClient): - """A set of methods to access the APIs of SystemLink Feeds Client.""" - def __init__(self, configuration: Optional[core.HttpConfiguration] = None): """Initialize an instance. @@ -26,7 +24,7 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None): ApiException: if unable to communicate with the Feeds Service. """ if configuration is None: - configuration = core.JupyterHttpConfiguration() + configuration = core.HttpConfigurationManager.get_configuration() super().__init__(configuration, base_path="/nifeed/v1/") @@ -35,10 +33,13 @@ def create_feed(self, feed: models.CreateFeedRequest) -> models.Feed: """Create a new feed with the provided feed details. Args: - feeds (models.CreateFeedsRequest): Request model to create the feed. + feeds (models.CreateFeedsRequest): Request to create the feed. Returns: models.Feed: Details of the created feed. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ ... @@ -56,6 +57,9 @@ def __query_feeds( Returns: models.QueryFeedsResponse: List of feeds. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ ... @@ -64,7 +68,7 @@ def query_feeds( platform: Optional[models.Platform] = None, workspace: Optional[str] = None, ) -> List[models.Feed]: - """Get a set of feeds based on the provided `platform` and `workspace`. + """Lists available feeds for the Platform `platform` under the Workspace `workspace`. Args: platform (Optional[models.Platform]): Information about system platform. \ @@ -73,6 +77,9 @@ def query_feeds( Returns: List[models.Feed]: List of feeds. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ platform_by_str = platform.value if platform is not None else None response = self.__query_feeds( @@ -102,6 +109,9 @@ def __upload_package( Returns: models.Package: Uploaded package response information. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ ... @@ -121,6 +131,9 @@ def upload_package( Returns: models.Package: Uploaded package information. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ response = self.__upload_package( feed_id=feed_id, @@ -146,6 +159,9 @@ def upload_package_content( Returns: models.Package: Uploaded package information. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ response = self.__upload_package( feed_id=feed_id, @@ -167,5 +183,8 @@ def delete_feed(self, id: str) -> None: Returns: None. + + Raises: + ApiException: if unable to communicate with the Feeds Service. """ ... diff --git a/nisystemlink/clients/feeds/models/__init__.py b/nisystemlink/clients/feeds/models/__init__.py index 841daa38..b1bdfeff 100644 --- a/nisystemlink/clients/feeds/models/__init__.py +++ b/nisystemlink/clients/feeds/models/__init__.py @@ -1,9 +1,7 @@ -from ._feeds_models import ( - CreateFeedRequest, - Feed, - Package, - Platform, - QueryFeedsResponse, -) +from ._create_feeds import CreateFeedRequest +from ._feed import Feed +from ._package import Package +from ._platform import Platform +from ._query_feeds import QueryFeedsResponse # flake8: noqa diff --git a/nisystemlink/clients/feeds/models/_create_feeds.py b/nisystemlink/clients/feeds/models/_create_feeds.py new file mode 100644 index 00000000..3551b4c0 --- /dev/null +++ b/nisystemlink/clients/feeds/models/_create_feeds.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + +from ._platform import Platform + + +class CreateFeedRequest(JsonModel): + """Create Feed Request.""" + + name: str + """The name of the feed.""" + description: Optional[str] = None + """The description of the feed.""" + platform: Platform + """The platform of the feed, the following package extensions are available: .nipkg for + windows feeds, .ipk and .deb for ni-linux-rt feeds.""" + workspace: Optional[str] = None + """The ID of the workspace this feed belongs to. If the workspace is not defined, + the default workspace is used.""" diff --git a/nisystemlink/clients/feeds/models/_feed.py b/nisystemlink/clients/feeds/models/_feed.py new file mode 100644 index 00000000..8a799583 --- /dev/null +++ b/nisystemlink/clients/feeds/models/_feed.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + +from ._platform import Platform + + +class Feed(JsonModel): + """Feed model.""" + + id: Optional[str] = None + """The auto-generated ID of the feed.""" + name: Optional[str] = None + """The name of the feed.""" + description: Optional[str] = None + """The description of the feed.""" + platform: Platform + """The platform of the feed, the following package extensions are available: .nipkg for + windows feeds, .ipk and .deb for ni-linux-rt feeds. + """ + workspace: Optional[str] = None + """The ID of the workspace this feed belongs to.""" + updated_at: str + """The date of the latest feed update""" + created_at: str + """The date when the feed was created at.""" + package_sources: Optional[List[str]] = None + """The package sources list of the feed.""" + deleted: bool + """Whether the feed deletion was requested.""" diff --git a/nisystemlink/clients/feeds/models/_feeds_models.py b/nisystemlink/clients/feeds/models/_package.py similarity index 57% rename from nisystemlink/clients/feeds/models/_feeds_models.py rename to nisystemlink/clients/feeds/models/_package.py index 3fbc6280..4b31b4da 100644 --- a/nisystemlink/clients/feeds/models/_feeds_models.py +++ b/nisystemlink/clients/feeds/models/_package.py @@ -1,72 +1,14 @@ -"""Models utilized for Feeds in SystemLink.""" - from __future__ import annotations -from enum import Enum from typing import Dict, List, Optional from nisystemlink.clients.core._uplink._json_model import JsonModel -from pydantic import Field - - -class Platform(Enum): - """Platform.""" - - WINDOWS = "WINDOWS" - NI_LINUX_RT = "NI_LINUX_RT" - - -class CreateFeedRequest(JsonModel): - """Create Feed Request.""" - - name: str - """The name of the feed.""" - description: Optional[str] = None - """The description of the feed.""" - platform: Platform - """The platform of the feed, the following package extensions are available: .nipkg for - windows feeds, .ipk and .deb for ni-linux-rt feeds.""" - workspace: Optional[str] = None - """The ID of the workspace this feed belongs to. If the workspace is not defined, - the default workspace is used.""" - - -class Feed(JsonModel): - """Feed model.""" - - id: Optional[str] = None - """The auto-generated ID of the feed.""" - name: Optional[str] = None - """The name of the feed.""" - description: Optional[str] = None - """The description of the feed.""" - platform: Platform - """The platform of the feed, the following package extensions are available: .nipkg for - windows feeds, .ipk and .deb for ni-linux-rt feeds. - """ - workspace: Optional[str] = None - """The ID of the workspace this feed belongs to.""" - updated_at: str = Field(alias="updatedAt") - """The date of the latest feed update""" - created_at: str = Field(alias="createdAt") - """The date when the feed was created at.""" - package_sources: Optional[List[str]] = Field(default=None, alias="packageSources") - """The package sources list of the feed.""" - deleted: bool - """Whether the feed deletion was requested.""" - - -class QueryFeedsResponse(JsonModel): - """Query Feeds response.""" - - feeds: List[Feed] - """A collection of feeds""" class PackageMetadata(JsonModel): """Package Meta data.""" - package_name: Optional[str] = Field(default=None, alias="packageName") + package_name: Optional[str] = None """The name of the package.""" version: Optional[str] = None """The version number of the package.""" @@ -84,13 +26,13 @@ class PackageMetadata(JsonModel): """Information about other packages this package enchances.""" essential: Optional[bool] = None """True if the package is essential.""" - file_name: Optional[str] = Field(default=None, alias="fileName") + file_name: Optional[str] = None """The file name of the package. Depending on the selected platform, the following package extensions are available: .nipkg for windows feeds, .ipk and .deb for ni-linux-rt feeds.""" homepage: Optional[str] = None """The website of the maintainers for the package.""" - installed_size: Optional[int] = Field(default=None, alias="installedSize") + installed_size: Optional[int] = None """The size of the package after install.""" maintainer: Optional[str] = None """The maintainer of the package (name and email address).""" @@ -102,7 +44,7 @@ class PackageMetadata(JsonModel): """Information about other packages that this package provides.""" recommends: Optional[List[str]] = None """Information about other packages this package recommends.""" - release_notes: Optional[str] = Field(default=None, alias="releaseNotes") + release_notes: Optional[str] = None """The release notes of the package.""" replaces: Optional[List[str]] = None """Information about other packages this package replaces.""" @@ -125,16 +67,16 @@ class Package(JsonModel): id: Optional[str] = None """Gets or sets the ID of this package. This is used to reference this package in the service.""" - file_name: Optional[str] = Field(default=None, alias="fileName") + file_name: Optional[str] = None """The name of the file in this package.""" - feed_id: Optional[str] = Field(default=None, alias="feedId") + feed_id: Optional[str] = None """The ID of the feed this package is associated with.""" workspace: Optional[str] = None """The ID of the workspace this package belongs to. The workspace of a package is the workspace of feed this package is associated with.""" - updated_at: str = Field(alias="updatedAt") + updated_at: str """The date of the latest package update.""" - created_at: str = Field(alias="createdAt") + created_at: str """The date when the package was created at.""" metadata: Optional[PackageMetadata] = None """Package meta data.""" diff --git a/nisystemlink/clients/feeds/models/_platform.py b/nisystemlink/clients/feeds/models/_platform.py new file mode 100644 index 00000000..d1a68a82 --- /dev/null +++ b/nisystemlink/clients/feeds/models/_platform.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from enum import Enum + + +class Platform(Enum): + """Platform.""" + + WINDOWS = "WINDOWS" + NI_LINUX_RT = "NI_LINUX_RT" diff --git a/nisystemlink/clients/feeds/models/_query_feeds.py b/nisystemlink/clients/feeds/models/_query_feeds.py new file mode 100644 index 00000000..1b5c51a7 --- /dev/null +++ b/nisystemlink/clients/feeds/models/_query_feeds.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import List + +from nisystemlink.clients.core._uplink._json_model import JsonModel + +from ._feed import Feed + + +class QueryFeedsResponse(JsonModel): + """Query Feeds response.""" + + feeds: List[Feed] + """A collection of feeds""" diff --git a/nisystemlink/clients/feeds/utilities.py b/nisystemlink/clients/feeds/utilities.py index 12e1a0bb..53fbd764 100644 --- a/nisystemlink/clients/feeds/utilities.py +++ b/nisystemlink/clients/feeds/utilities.py @@ -9,10 +9,10 @@ def get_feed_by_name( feeds: List[Feed], name: str, ) -> Optional[Feed]: - """Get feed id from the list of feed details using `feed_name`. + """Get feed id from the list of feeds using `feed_name`. Args: - feeds (List[Feed]): List of feed details. + feeds (List[Feed]): List of feeds. feed_name (str): Feed name. Returns: diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index f0b64a51..380774eb 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -88,8 +88,6 @@ def _get_feed_name(): @pytest.mark.enterprise @pytest.mark.integration class TestFeedsClient: - """A set of test methods to test SystemLink Feeds API.""" - def test__create_feed_windows_platform__succeeds( self, create_feed: Callable, @@ -97,8 +95,9 @@ def test__create_feed_windows_platform__succeeds( get_feed_name: Callable, ): """Test the case of a completely successful create feed API for Windows platform.""" + feed_name = get_feed_name() request_body = create_feed_request( - feed_name=get_feed_name(), + feed_name=feed_name, description=FEED_DESCRIPTION, platform=Platform.WINDOWS, ) @@ -106,7 +105,7 @@ def test__create_feed_windows_platform__succeeds( assert response.id is not None assert response.workspace is not None - assert response.name is not None + assert response.name == feed_name assert response.platform == Platform.WINDOWS assert response.description == FEED_DESCRIPTION @@ -117,16 +116,17 @@ def test__create_feed_linux_platform__succeeds( get_feed_name: Callable, ): """Test the case of a completely successful create feed API for Linux platform.""" + feed_name = get_feed_name() request_body = create_feed_request( - feed_name=get_feed_name(), + feed_name=feed_name, description=FEED_DESCRIPTION, - platform=Platform.NI_LINUX_RT, + platform=Platform.WINDOWS, ) response = create_feed(request_body) assert response.id is not None assert response.workspace is not None - assert response.name is not None + assert response.name == feed_name assert response.platform == Platform.NI_LINUX_RT assert response.description == FEED_DESCRIPTION @@ -143,7 +143,8 @@ def test__query_feeds_windows_platform__succeeds( description=FEED_DESCRIPTION, platform=Platform.WINDOWS, ) - create_feed(create_feed_request_body) + create_feed_resp = create_feed(create_feed_request_body) + assert create_feed_resp.id is not None query_feed_resp = client.query_feeds(platform=Platform.WINDOWS) assert query_feed_resp is not None @@ -161,7 +162,8 @@ def test__query_feeds_linux_platform__succeeds( description=FEED_DESCRIPTION, platform=Platform.NI_LINUX_RT, ) - create_feed(create_feed_request_body) + create_feed_resp = create_feed(create_feed_request_body) + assert create_feed_resp.id is not None query_feed_resp = client.query_feeds(platform=Platform.NI_LINUX_RT) assert query_feed_resp is not None @@ -189,6 +191,7 @@ def test__upload_package__succeeds( platform=Platform.WINDOWS, ) create_feed_resp = create_feed(create_feed_request_body) + assert create_feed_resp.id is not None upload_pacakge_rsp = client.upload_package( package_file_path=PACKAGE_PATH, @@ -212,6 +215,7 @@ def test__upload_package_content__succeeds( platform=Platform.WINDOWS, ) create_feed_resp = create_feed(create_feed_request_body) + assert create_feed_resp.id is not None upload_pacakge_rsp = client.upload_package_content( package=binary_pkg_file_data, @@ -246,10 +250,10 @@ def test__delete_windows_feed__succeeds( platform=Platform.WINDOWS, ) create_feed_resp = client.create_feed(create_feed_request_body) + assert create_feed_resp.id is not None - if create_feed_resp.id: - delete_feed_resp = client.delete_feed(id=create_feed_resp.id) - assert delete_feed_resp is None + delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None def test__delete__linux_feed__succeeds( self, @@ -264,7 +268,7 @@ def test__delete__linux_feed__succeeds( platform=Platform.NI_LINUX_RT, ) create_feed_resp = client.create_feed(create_feed_request_body) + assert create_feed_resp.id is not None - if create_feed_resp.id: - delete_feed_resp = client.delete_feed(id=create_feed_resp.id) - assert delete_feed_resp is None + delete_feed_resp = client.delete_feed(id=create_feed_resp.id) + assert delete_feed_resp is None From 5e0752ddc1f4bd0010f9ffdec0b169ac34242424 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 10:51:13 +0530 Subject: [PATCH 34/44] fix: pytest failure due to invalid assertion --- tests/integration/feeds/test_feeds_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 380774eb..181c38bf 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -73,7 +73,7 @@ def invalid_id() -> str: @pytest.fixture(scope="class") def get_feed_name(): """Generate a feed name.""" - name = "Test_Feed_" + name = "test_feed_" feed_count = 0 def _get_feed_name(): @@ -120,7 +120,7 @@ def test__create_feed_linux_platform__succeeds( request_body = create_feed_request( feed_name=feed_name, description=FEED_DESCRIPTION, - platform=Platform.WINDOWS, + platform=Platform.NI_LINUX_RT, ) response = create_feed(request_body) From 6b7f2a1fe50a8a42fd85498533360f0190b0ed52 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 14:00:18 +0530 Subject: [PATCH 35/44] fix: address self review comments --- examples/feeds/create_feed.py | 17 ++++++++--------- examples/feeds/delete_feed.py | 15 +++++++-------- examples/feeds/query_and_upload_feeds.py | 21 ++++++++++----------- nisystemlink/clients/feeds/_feeds_client.py | 4 ++-- nisystemlink/clients/feeds/utilities.py | 4 ++-- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index 40498672..70ec0600 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -7,19 +7,18 @@ Platform, ) - -FEED_NAME = "EXAMPLE FEED" -FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" +# Update the constants. +FEED_NAME = "" +FEED_DESCRIPTION = "" PLATFORM = Platform.WINDOWS +WORKSPACE_ID = ( + None # None uses Default workspace. Replace with Systemlink workspace id. +) server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -workspace_id = ( - None # None uses Default workspace. Replace with Systemlink workspace id. -) - -# Please provide the valid API key and API URL for client intialization. +# Provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # Creating Feeds. @@ -28,7 +27,7 @@ name=FEED_NAME, description=FEED_DESCRIPTION, platform=PLATFORM, - workspace=workspace_id, + workspace=WORKSPACE_ID, ) feed_details = client.create_feed(feed=feed_request) diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index 75624353..c828965c 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -5,24 +5,23 @@ from nisystemlink.clients.feeds.models import Platform from nisystemlink.clients.feeds.utilities import get_feed_by_name - -FEED_NAME = "EXAMPLE FEED" # Name of the feed. +# Update the constants. +FEED_NAME = "" PLATFORM = Platform.WINDOWS +WORKSPACE_ID = ( + None # None uses Default workspace. Replace with Systemlink workspace id. +) server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -workspace_id = ( - None # None uses Default workspace. Replace with Systemlink workspace id. -) - -# Please provide the valid API key and API URL for client intialization. +# Provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # Deleting Feed. try: # Get ID of the Feed to delete by name - feeds = client.query_feeds(platform=PLATFORM, workspace=workspace_id) + feeds = client.query_feeds(platform=PLATFORM, workspace=WORKSPACE_ID) feed = get_feed_by_name(feeds=feeds, name=FEED_NAME) feed_id = feed.id if feed else None diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index 5ad7d072..b975b0fd 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -5,27 +5,26 @@ from nisystemlink.clients.feeds.models import Platform from nisystemlink.clients.feeds.utilities import get_feed_by_name -# Constant -FEED_NAME = "EXAMPLE FEED" -FEED_DESCRIPTION = "EXAMPLE DESCRIPTION" +# Update the constants. +FEED_NAME = "" +PLATFORM = None +FEED_DESCRIPTION = "" PLATFORM = Platform.WINDOWS -PACKAGE_NAME = "" +WORKSPACE_ID = ( + None # None uses Default workspace. Replace with Systemlink workspace id. +) PACKAGE_PATH = "" server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -workspace_id = ( - None # None uses Default workspace. Replace with Systemlink workspace id. -) - -# Please provide the valid API key and API URL for client intialization. +# Provide the valid API key and API URL for client intialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # To upload a package to feed. try: - # Get ID of the Feed to delete by name - feeds = client.query_feeds(platform=PLATFORM, workspace=workspace_id) + # Get ID of the Feed to upload by name + feeds = client.query_feeds(platform=PLATFORM, workspace=WORKSPACE_ID) feed = get_feed_by_name(feeds=feeds, name=FEED_NAME) feed_id = feed.id if feed else None diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index 4d7f11c2..f67f62f0 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -49,7 +49,7 @@ def __query_feeds( platform: Optional[str] = None, workspace: Optional[str] = None, ) -> models.QueryFeedsResponse: - """Get a set of feeds based on the provided `platform` and `workspace`. + """Available feeds for the Platform `platform` under the Workspace `workspace` Args: platform (Optional[str]): Information about system platform. Defaults to None. @@ -108,7 +108,7 @@ def __upload_package( Defaults to False. Returns: - models.Package: Uploaded package response information. + models.Package: Uploaded package information. Raises: ApiException: if unable to communicate with the Feeds Service. diff --git a/nisystemlink/clients/feeds/utilities.py b/nisystemlink/clients/feeds/utilities.py index 53fbd764..e56fc647 100644 --- a/nisystemlink/clients/feeds/utilities.py +++ b/nisystemlink/clients/feeds/utilities.py @@ -9,11 +9,11 @@ def get_feed_by_name( feeds: List[Feed], name: str, ) -> Optional[Feed]: - """Get feed id from the list of feeds using `feed_name`. + """Get feed id from the list of feeds using `name`. Args: feeds (List[Feed]): List of feeds. - feed_name (str): Feed name. + name (str): Feed name. Returns: Optional[Feed]: Feed information. From 26af9a63b4213655d3ca2ea759d5fdbe52da86e6 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 14:06:00 +0530 Subject: [PATCH 36/44] fix: issue due to black formatter --- examples/feeds/create_feed.py | 4 ++-- examples/feeds/delete_feed.py | 4 ++-- examples/feeds/query_and_upload_feeds.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/feeds/create_feed.py b/examples/feeds/create_feed.py index 70ec0600..5eaaf78b 100644 --- a/examples/feeds/create_feed.py +++ b/examples/feeds/create_feed.py @@ -12,13 +12,13 @@ FEED_DESCRIPTION = "" PLATFORM = Platform.WINDOWS WORKSPACE_ID = ( - None # None uses Default workspace. Replace with Systemlink workspace id. + None # None uses Default workspace. Replace with Systemlink workspace id. ) server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -# Provide the valid API key and API URL for client intialization. +# Provide the valid API key and API URL for client initialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # Creating Feeds. diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index c828965c..a1085346 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -9,13 +9,13 @@ FEED_NAME = "" PLATFORM = Platform.WINDOWS WORKSPACE_ID = ( - None # None uses Default workspace. Replace with Systemlink workspace id. + None # None uses Default workspace. Replace with Systemlink workspace id. ) server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -# Provide the valid API key and API URL for client intialization. +# Provide the valid API key and API URL for client initialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # Deleting Feed. diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index b975b0fd..c5f3ce8a 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -11,14 +11,14 @@ FEED_DESCRIPTION = "" PLATFORM = Platform.WINDOWS WORKSPACE_ID = ( - None # None uses Default workspace. Replace with Systemlink workspace id. + None # None uses Default workspace. Replace with Systemlink workspace id. ) PACKAGE_PATH = "" server_url = "" # SystemLink API URL server_api_key = "" # SystemLink API key -# Provide the valid API key and API URL for client intialization. +# Provide the valid API key and API URL for client initialization. client = FeedsClient(HttpConfiguration(api_key=server_api_key, server_uri=server_url)) # To upload a package to feed. From 33c80fb0ad0942776b4dcb444c26c295e4d1d86a Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 14:12:04 +0530 Subject: [PATCH 37/44] fix: pipeline failure issue --- tests/integration/feeds/test_feeds_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 181c38bf..49c399d3 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -73,7 +73,7 @@ def invalid_id() -> str: @pytest.fixture(scope="class") def get_feed_name(): """Generate a feed name.""" - name = "test_feed_" + name = "test_feed" feed_count = 0 def _get_feed_name(): From 44472b72e1d410b9cc0edf8420c2bb8a848a119a Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 19:25:44 +0530 Subject: [PATCH 38/44] fix: address self review comments --- nisystemlink/clients/feeds/_feeds_client.py | 4 ++-- nisystemlink/clients/feeds/models/__init__.py | 2 +- .../feeds/models/{_create_feeds.py => _create_feed.py} | 0 nisystemlink/clients/feeds/utilities.py | 2 +- tests/integration/feeds/test_feeds_client.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename nisystemlink/clients/feeds/models/{_create_feeds.py => _create_feed.py} (100%) diff --git a/nisystemlink/clients/feeds/_feeds_client.py b/nisystemlink/clients/feeds/_feeds_client.py index f67f62f0..df4d6ac5 100644 --- a/nisystemlink/clients/feeds/_feeds_client.py +++ b/nisystemlink/clients/feeds/_feeds_client.py @@ -49,7 +49,7 @@ def __query_feeds( platform: Optional[str] = None, workspace: Optional[str] = None, ) -> models.QueryFeedsResponse: - """Available feeds for the Platform `platform` under the Workspace `workspace` + """Lists available feeds for the Platform `platform` under the Workspace `workspace`. Args: platform (Optional[str]): Information about system platform. Defaults to None. @@ -103,7 +103,7 @@ def __upload_package( Args: feed_id (str): ID of the feed. - package (Part): Package file as a form data. + package (Part): Package file to be uploaded. overwrite (Query): Set to True, to overwrite the package if it already exists.\ Defaults to False. diff --git a/nisystemlink/clients/feeds/models/__init__.py b/nisystemlink/clients/feeds/models/__init__.py index b1bdfeff..84c1df08 100644 --- a/nisystemlink/clients/feeds/models/__init__.py +++ b/nisystemlink/clients/feeds/models/__init__.py @@ -1,4 +1,4 @@ -from ._create_feeds import CreateFeedRequest +from ._create_feed import CreateFeedRequest from ._feed import Feed from ._package import Package from ._platform import Platform diff --git a/nisystemlink/clients/feeds/models/_create_feeds.py b/nisystemlink/clients/feeds/models/_create_feed.py similarity index 100% rename from nisystemlink/clients/feeds/models/_create_feeds.py rename to nisystemlink/clients/feeds/models/_create_feed.py diff --git a/nisystemlink/clients/feeds/utilities.py b/nisystemlink/clients/feeds/utilities.py index e56fc647..4b1a5776 100644 --- a/nisystemlink/clients/feeds/utilities.py +++ b/nisystemlink/clients/feeds/utilities.py @@ -9,7 +9,7 @@ def get_feed_by_name( feeds: List[Feed], name: str, ) -> Optional[Feed]: - """Get feed id from the list of feeds using `name`. + """Get feed information from the list of feeds using `name`. Args: feeds (List[Feed]): List of feeds. diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 49c399d3..4a105ae8 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -73,7 +73,7 @@ def invalid_id() -> str: @pytest.fixture(scope="class") def get_feed_name(): """Generate a feed name.""" - name = "test_feed" + name = "test_feed__" feed_count = 0 def _get_feed_name(): From da58c2867ea8b896c77f1fd5bc38293f48343a6e Mon Sep 17 00:00:00 2001 From: Giriharan Date: Tue, 5 Nov 2024 21:50:02 +0530 Subject: [PATCH 39/44] fix: pipeline failure --- tests/integration/feeds/test_feeds_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/feeds/test_feeds_client.py b/tests/integration/feeds/test_feeds_client.py index 4a105ae8..4639a083 100644 --- a/tests/integration/feeds/test_feeds_client.py +++ b/tests/integration/feeds/test_feeds_client.py @@ -73,7 +73,7 @@ def invalid_id() -> str: @pytest.fixture(scope="class") def get_feed_name(): """Generate a feed name.""" - name = "test_feed__" + name = "testfeed_" feed_count = 0 def _get_feed_name(): From 75bef43fdd2de4c11f5bdc603ee86056b9c1d582 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 7 Nov 2024 09:38:45 +0530 Subject: [PATCH 40/44] fix: address final PR comments --- examples/feeds/delete_feed.py | 2 +- examples/feeds/query_and_upload_feeds.py | 2 +- nisystemlink/clients/feeds/models/_feed.py | 9 +++++---- nisystemlink/clients/feeds/models/_package.py | 5 +++-- nisystemlink/clients/feeds/utilities/__init__.py | 3 +++ .../{utilities.py => utilities/_get_feed_details.py} | 0 6 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 nisystemlink/clients/feeds/utilities/__init__.py rename nisystemlink/clients/feeds/{utilities.py => utilities/_get_feed_details.py} (100%) diff --git a/examples/feeds/delete_feed.py b/examples/feeds/delete_feed.py index a1085346..030c416a 100644 --- a/examples/feeds/delete_feed.py +++ b/examples/feeds/delete_feed.py @@ -3,7 +3,7 @@ from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import Platform -from nisystemlink.clients.feeds.utilities import get_feed_by_name +from nisystemlink.clients.feeds.utilities._get_feed_details import get_feed_by_name # Update the constants. FEED_NAME = "" diff --git a/examples/feeds/query_and_upload_feeds.py b/examples/feeds/query_and_upload_feeds.py index c5f3ce8a..d69fa22a 100644 --- a/examples/feeds/query_and_upload_feeds.py +++ b/examples/feeds/query_and_upload_feeds.py @@ -3,7 +3,7 @@ from nisystemlink.clients.core import ApiException, HttpConfiguration from nisystemlink.clients.feeds._feeds_client import FeedsClient from nisystemlink.clients.feeds.models import Platform -from nisystemlink.clients.feeds.utilities import get_feed_by_name +from nisystemlink.clients.feeds.utilities._get_feed_details import get_feed_by_name # Update the constants. FEED_NAME = "" diff --git a/nisystemlink/clients/feeds/models/_feed.py b/nisystemlink/clients/feeds/models/_feed.py index 8a799583..e894db56 100644 --- a/nisystemlink/clients/feeds/models/_feed.py +++ b/nisystemlink/clients/feeds/models/_feed.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from typing import List, Optional from nisystemlink.clients.core._uplink._json_model import JsonModel @@ -16,17 +17,17 @@ class Feed(JsonModel): """The name of the feed.""" description: Optional[str] = None """The description of the feed.""" - platform: Platform + platform: Optional[Platform] = None """The platform of the feed, the following package extensions are available: .nipkg for windows feeds, .ipk and .deb for ni-linux-rt feeds. """ workspace: Optional[str] = None """The ID of the workspace this feed belongs to.""" - updated_at: str + updated_at: Optional[datetime] = None """The date of the latest feed update""" - created_at: str + created_at: Optional[datetime] = None """The date when the feed was created at.""" package_sources: Optional[List[str]] = None """The package sources list of the feed.""" - deleted: bool + deleted: Optional[bool] = None """Whether the feed deletion was requested.""" diff --git a/nisystemlink/clients/feeds/models/_package.py b/nisystemlink/clients/feeds/models/_package.py index 4b31b4da..0dc00cb8 100644 --- a/nisystemlink/clients/feeds/models/_package.py +++ b/nisystemlink/clients/feeds/models/_package.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from typing import Dict, List, Optional from nisystemlink.clients.core._uplink._json_model import JsonModel @@ -74,9 +75,9 @@ class Package(JsonModel): workspace: Optional[str] = None """The ID of the workspace this package belongs to. The workspace of a package is the workspace of feed this package is associated with.""" - updated_at: str + updated_at: Optional[datetime] """The date of the latest package update.""" - created_at: str + created_at: Optional[datetime] """The date when the package was created at.""" metadata: Optional[PackageMetadata] = None """Package meta data.""" diff --git a/nisystemlink/clients/feeds/utilities/__init__.py b/nisystemlink/clients/feeds/utilities/__init__.py new file mode 100644 index 00000000..457eda5a --- /dev/null +++ b/nisystemlink/clients/feeds/utilities/__init__.py @@ -0,0 +1,3 @@ +from ._get_feed_details import get_feed_by_name + +# flake8: noqa \ No newline at end of file diff --git a/nisystemlink/clients/feeds/utilities.py b/nisystemlink/clients/feeds/utilities/_get_feed_details.py similarity index 100% rename from nisystemlink/clients/feeds/utilities.py rename to nisystemlink/clients/feeds/utilities/_get_feed_details.py From 14b14015a03d20526ef9f3a1d72541e028f6b509 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 7 Nov 2024 09:52:12 +0530 Subject: [PATCH 41/44] fix: issue due to black formatter --- nisystemlink/clients/feeds/utilities/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nisystemlink/clients/feeds/utilities/__init__.py b/nisystemlink/clients/feeds/utilities/__init__.py index 457eda5a..0d07a5c4 100644 --- a/nisystemlink/clients/feeds/utilities/__init__.py +++ b/nisystemlink/clients/feeds/utilities/__init__.py @@ -1,3 +1,3 @@ from ._get_feed_details import get_feed_by_name -# flake8: noqa \ No newline at end of file +# flake8: noqa From 4f683f97d9ebe5dbbdfd1613c767230e369226a7 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 7 Nov 2024 10:38:47 +0530 Subject: [PATCH 42/44] fix: update response models --- nisystemlink/clients/feeds/models/_package.py | 2 +- nisystemlink/clients/feeds/models/_query_feeds.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nisystemlink/clients/feeds/models/_package.py b/nisystemlink/clients/feeds/models/_package.py index 0dc00cb8..ec60e3ea 100644 --- a/nisystemlink/clients/feeds/models/_package.py +++ b/nisystemlink/clients/feeds/models/_package.py @@ -39,7 +39,7 @@ class PackageMetadata(JsonModel): """The maintainer of the package (name and email address).""" predepends: Optional[List[str]] = None """Information about other packages this package predepends.""" - priority: int + priority: Optional[int] = None """The priority of the package.""" provides: Optional[List[str]] = None """Information about other packages that this package provides.""" diff --git a/nisystemlink/clients/feeds/models/_query_feeds.py b/nisystemlink/clients/feeds/models/_query_feeds.py index 1b5c51a7..b90cc491 100644 --- a/nisystemlink/clients/feeds/models/_query_feeds.py +++ b/nisystemlink/clients/feeds/models/_query_feeds.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List +from typing import List, Optional from nisystemlink.clients.core._uplink._json_model import JsonModel @@ -10,5 +10,5 @@ class QueryFeedsResponse(JsonModel): """Query Feeds response.""" - feeds: List[Feed] + feeds: Optional[List[Feed]] = None """A collection of feeds""" From db529c8618cc61545440c317e89d90aa45040ea3 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 7 Nov 2024 11:47:41 +0530 Subject: [PATCH 43/44] fix: update feeds model with some fixes --- nisystemlink/clients/feeds/models/_package.py | 4 ++-- nisystemlink/clients/feeds/models/_query_feeds.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nisystemlink/clients/feeds/models/_package.py b/nisystemlink/clients/feeds/models/_package.py index ec60e3ea..c119de1a 100644 --- a/nisystemlink/clients/feeds/models/_package.py +++ b/nisystemlink/clients/feeds/models/_package.py @@ -75,9 +75,9 @@ class Package(JsonModel): workspace: Optional[str] = None """The ID of the workspace this package belongs to. The workspace of a package is the workspace of feed this package is associated with.""" - updated_at: Optional[datetime] + updated_at: Optional[datetime] = None """The date of the latest package update.""" - created_at: Optional[datetime] + created_at: Optional[datetime] = None """The date when the package was created at.""" metadata: Optional[PackageMetadata] = None """Package meta data.""" diff --git a/nisystemlink/clients/feeds/models/_query_feeds.py b/nisystemlink/clients/feeds/models/_query_feeds.py index b90cc491..1b5c51a7 100644 --- a/nisystemlink/clients/feeds/models/_query_feeds.py +++ b/nisystemlink/clients/feeds/models/_query_feeds.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Optional +from typing import List from nisystemlink.clients.core._uplink._json_model import JsonModel @@ -10,5 +10,5 @@ class QueryFeedsResponse(JsonModel): """Query Feeds response.""" - feeds: Optional[List[Feed]] = None + feeds: List[Feed] """A collection of feeds""" From f639d7734760a6812e01920bb0a9c9e9226c8154 Mon Sep 17 00:00:00 2001 From: Giriharan Date: Thu, 7 Nov 2024 18:18:56 +0530 Subject: [PATCH 44/44] docs: update utility doc string --- nisystemlink/clients/feeds/utilities/_get_feed_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nisystemlink/clients/feeds/utilities/_get_feed_details.py b/nisystemlink/clients/feeds/utilities/_get_feed_details.py index 4b1a5776..c25dcddd 100644 --- a/nisystemlink/clients/feeds/utilities/_get_feed_details.py +++ b/nisystemlink/clients/feeds/utilities/_get_feed_details.py @@ -1,4 +1,4 @@ -"""Utilities for FeedsClient.""" +"""Utilities for getting feed details.""" from typing import List, Optional