From 3e9a1243f715690d94adb6e7648b5c760e075eda Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 29 Dec 2023 05:16:47 -0500 Subject: [PATCH] feat: add pre-commit integrations (#49) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kalen Michael --- .pre-commit-config.yaml | 92 +++++++++++++++++ README.md | 9 +- docs/index.md | 8 +- docs/model.md | 6 +- docs/quickstart.md | 8 +- docs/reference.md | 2 +- hub_sdk/__init__.py | 2 +- hub_sdk/base/api_client.py | 50 ++++----- hub_sdk/base/auth.py | 32 +++--- hub_sdk/base/crud_client.py | 29 +++--- hub_sdk/base/paginated_list.py | 38 ++++--- hub_sdk/base/server_clients.py | 90 ++++++++-------- hub_sdk/config.py | 9 +- hub_sdk/helpers/error_handler.py | 15 +-- hub_sdk/helpers/exceptions.py | 3 +- hub_sdk/helpers/logger.py | 7 +- hub_sdk/helpers/utils.py | 7 +- hub_sdk/hub_client.py | 44 ++++---- hub_sdk/modules/datasets.py | 32 +++--- hub_sdk/modules/models.py | 72 +++++++------ hub_sdk/modules/projects.py | 20 ++-- hub_sdk/modules/teams.py | 21 ++-- setup.cfg | 69 +++++++++++++ setup.py | 47 +++------ tests/test.py | 172 ------------------------------- tests/testCase_dataset.py | 33 +++--- tests/testCase_model.py | 59 ++++++----- tests/testCase_project.py | 29 +++--- 28 files changed, 507 insertions(+), 498 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..079a413 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,92 @@ +# Ultralytics YOLO πŸš€, AGPL-3.0 license +# Pre-commit hooks. For more information see https://github.com/pre-commit/pre-commit-hooks/blob/main/README.md +# Optionally remove from local hooks with 'rm .git/hooks/pre-commit' + +# Define bot property if installed via https://github.com/marketplace/pre-commit-ci +ci: + autofix_prs: true + autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions' + autoupdate_schedule: monthly + submodules: true + +# Exclude directories from checks (optional) +# exclude: 'docs/' + +# Define repos to run +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-case-conflict + # - id: check-yaml + - id: check-docstring-first + - id: double-quote-string-fixer + - id: detect-private-key + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + name: Upgrade code + + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + name: Sort imports + + - repo: https://github.com/google/yapf + rev: v0.40.2 + hooks: + - id: yapf + name: YAPF formatting + + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + name: MD formatting + additional_dependencies: + - mdformat-gfm + # - mdformat-black + # - mdformat-frontmatter + args: + - --wrap=no + exclude: 'docs/.*\.md' + # exclude: "README.md|README.zh-CN.md|CONTRIBUTING.md" + + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + name: PEP8 + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + exclude: 'docs/de|docs/fr|docs/pt|docs/es|docs/mkdocs_de.yml' + args: + - --ignore-words-list=crate,nd,strack,dota,ane,segway,fo,gool,winn + + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + +# - repo: https://github.com/asottile/yesqa +# rev: v1.4.0 +# hooks: +# - id: yesqa + +# - repo: https://github.com/asottile/dead +# rev: v1.5.0 +# hooks: +# - id: dead + +# - repo: https://github.com/ultralytics/pre-commit +# rev: bd60a414f80a53fb8f593d3bfed4701fc47e4b23 +# hooks: +# - id: capitalize-comments diff --git a/README.md b/README.md index 17d85b3..921fe59 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,19 @@ Welcome to the Ultralytics HUB-SDK documentation! πŸ“– Our aim is to guide you t Ready to dive into HUB-SDK? Follow these steps to set it up on your machine. ### Prerequisites + Ensure you have the following requirements met before proceeding: -- **Python:** HUB-SDK requires Python. Download and install Python from [python.org](https://www.python.org/downloads/) if it's not already installed on your system. +- **Python:** HUB-SDK requires Python. Download and install Python from [python.org](https://www.python.org/downloads/) if it's not already installed on your system. -- **Git (Optional):** If you're looking to install HUB-SDK via the GitHub repository, you'll need Git. Grab Git from [git-scm.com](https://git-scm.com/downloads) if you don't have it. +- **Git (Optional):** If you're looking to install HUB-SDK via the GitHub repository, you'll need Git. Grab Git from [git-scm.com](https://git-scm.com/downloads) if you don't have it. ### Installation Methods + Choose from the following options to install HUB-SDK: #### Installing from PyPI + For the latest stable release of HUB-SDK, use PyPI by running the following command: ```sh @@ -137,7 +140,7 @@ We're thrilled to have you contribute to Ultralytics' open-source projects! Your Ultralytics provides two types of licensing options: - **AGPL-3.0 License**: An [OSI-approved](https://opensource.org/licenses/) open-source license. Ideal for academics, researchers, and enthusiasts, this license promotes sharing knowledge and collaboration. See the [LICENSE](https://github.com/ultralytics/ultralytics/blob/main/LICENSE) file for details. - + - **Enterprise License**: Tailored for commercial applications, this license allows for the integration of Ultralytics software into proprietary products and services. If you're considering using our solutions commercially, please get in touch through [Ultralytics Licensing](https://ultralytics.com/license). ## 🀝 Contact diff --git a/docs/index.md b/docs/index.md index 4d615f4..fefec68 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,23 +12,23 @@ To install the latest stable release of HUB-SDK from PyPI, run the following com pip install hub-sdk ``` -## Initilize HUBClient +## Initialize HUBClient In the provided code snippet, you are attempting to initialize an HUBClient object, presumably for some kind of API or service access. You have two options for providing credentials: using an API key or using an email/password combination. ```sh -crednetials = {"api_key": ""} +credentials = {"api_key": ""} ``` In this option, you are initializing the HUBClient by providing an API key in the credentials dictionary. This is commonly used when you have an API that requires an API key for authentication. ```sh -crednetials = {"email": "", "password": ""} +credentials = {"email": "", "password": ""} ``` In this option, you are initializing the HUBClient by providing an email and password in the credentials dictionary. This is typically used when you need to authenticate using a username (email) and password combination. ```sh -client = HUBClient(crednetials) +client = HUBClient(credentials) ``` In this line of code, a client for the HUB service is being initialized with the provided credentials. The HUBClient class is used to create a connection to the HUB platform, with authentication details such as an API key or email/password pair stored in the credentials dictionary. diff --git a/docs/model.md b/docs/model.md index feb8c17..8f3381f 100644 --- a/docs/model.md +++ b/docs/model.md @@ -10,7 +10,7 @@ print(model.data) ### Project and Dataset Check This code snippet checks whether both a project and a dataset exist based on their respective IDs. It initializes project and dataset objects using their IDs and raises an exception if either of them is not available. ```sh -project = client.project("") +project = client.project("") dataset = client.dataset("") if None in (project.id, dataset.id): raise "Available" @@ -35,7 +35,7 @@ model = client.model() model.create_model(data) ``` -### Update Model +### Update Model This code demonstrates how to update the metadata of an existing model. You can change attributes like the model's name by specifying the model's ID and providing updated metadata. ```sh @@ -43,7 +43,7 @@ model = client.model("") model.update({"meta": {"name": "model Name"}}) ``` -### Delete Model +### Delete Model This function allows you to delete a specific model by providing its ID. Be cautious when using this function, as it permanently removes the model and its associated data. ```sh model = client.model("Model ID") diff --git a/docs/quickstart.md b/docs/quickstart.md index fb86f6e..308c640 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -23,24 +23,24 @@ pip install hub-sdk ``` -## Initilize HUBClient +## Initialize HUBClient In the provided code snippet, you are attempting to initialize an HUBClient object, presumably for some kind of API or service access. You have two options for providing credentials: using an API key or using an email/password combination. ```sh -crednetials = {"api_key": ""} +credentials = {"api_key": ""} ``` In this option, you are initializing the HUBClient by providing an API key in the credentials dictionary. This is commonly used when you have an API that requires an API key for authentication. ```sh -crednetials = {"email": "", "password": ""} +credentials = {"email": "", "password": ""} ``` In this option, you are initializing the HUBClient by providing an email and password in the credentials dictionary. This is typically used when you need to authenticate using a username (email) and password combination. ```sh -client = HUBClient(crednetials) +client = HUBClient(credentials) ``` In this line of code, a client for the HUB service is being initialized with the provided credentials. The HUBClient class is used to create a connection to the HUB platform, with authentication details such as an API key or email/password pair stored in the credentials dictionary. diff --git a/docs/reference.md b/docs/reference.md index 6371064..12adb4b 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1 +1 @@ -::: hub_sdk.hub_client.HUBClient \ No newline at end of file +::: hub_sdk.hub_client.HUBClient diff --git a/hub_sdk/__init__.py b/hub_sdk/__init__.py index cbf4644..b787a74 100644 --- a/hub_sdk/__init__.py +++ b/hub_sdk/__init__.py @@ -1,7 +1,7 @@ # Ultralytics HUB CLient SDK πŸš€ -from hub_sdk.hub_client import HUBClient from hub_sdk.config import HUB_API_ROOT, HUB_WEB_ROOT +from hub_sdk.hub_client import HUBClient __version__ = '1.0.0' __all__ = '__version__', 'HUBClient', 'HUB_API_ROOT', 'HUB_WEB_ROOT' diff --git a/hub_sdk/base/api_client.py b/hub_sdk/base/api_client.py index 00b701c..d5ae0a0 100644 --- a/hub_sdk/base/api_client.py +++ b/hub_sdk/base/api_client.py @@ -1,19 +1,23 @@ import requests -from hub_sdk.helpers.error_handler import ErrorHandler + from hub_sdk.config import HUB_EXCEPTIONS +from hub_sdk.helpers.error_handler import ErrorHandler from hub_sdk.helpers.logger import logger + class APIClientError(Exception): + def __init__(self, message: str, status_code: int = None): super().__init__(message) self.status_code = status_code self.message = message def __str__(self) -> str: - return f"{self.__class__.__name__}: {self.args[0]}" + return f'{self.__class__.__name__}: {self.args[0]}' class APIClient: + def __init__(self, base_url: str, headers: dict = None): """ Initialize an instance of the APIClient class. @@ -26,7 +30,14 @@ def __init__(self, base_url: str, headers: dict = None): self.headers = headers self.logger = logger - def _make_request(self, method: str, endpoint: str, data: dict=None, json=None, params=None, files=None, stream : bool=False): + def _make_request(self, + method: str, + endpoint: str, + data: dict = None, + json=None, + params=None, + files=None, + stream: bool = False): """ Make an HTTP request to the API. @@ -47,27 +58,18 @@ def _make_request(self, method: str, endpoint: str, data: dict=None, json=None, based on the HTTP status code. """ # Overwrite the base url if a http url is submitted - url = endpoint if endpoint.startswith("http") else self.base_url + endpoint + url = endpoint if endpoint.startswith('http') else self.base_url + endpoint - kwargs = { - "params": params, - "files": files, - "headers": self.headers, - "stream": stream - } + kwargs = {'params': params, 'files': files, 'headers': self.headers, 'stream': stream} # Determine the request data based on 'data' or 'json_data' if json is not None: - kwargs["json"] = json + kwargs['json'] = json else: - kwargs["data"] = data + kwargs['data'] = data try: - response = requests.request( - method, - url, - **kwargs - ) + response = requests.request(method, url, **kwargs) response.raise_for_status() return response @@ -92,9 +94,9 @@ def get(self, endpoint: str, params=None): Returns: requests.Response: The response object from the HTTP GET request. """ - return self._make_request("GET", endpoint, params=params) + return self._make_request('GET', endpoint, params=params) - def post(self, endpoint: str, data: dict =None, json=None, files=None): + def post(self, endpoint: str, data: dict = None, json=None, files=None): """ Make a POST request to the API. @@ -105,7 +107,7 @@ def post(self, endpoint: str, data: dict =None, json=None, files=None): Returns: requests.Response: The response object from the HTTP POST request. """ - return self._make_request("POST", endpoint, data=data, json=json, files=files) + return self._make_request('POST', endpoint, data=data, json=json, files=files) def put(self, endpoint: str, data=None, json=None): """ @@ -118,7 +120,7 @@ def put(self, endpoint: str, data=None, json=None): Returns: requests.Response: The response object from the HTTP PUT request. """ - return self._make_request("PUT", endpoint, data=data, json=json) + return self._make_request('PUT', endpoint, data=data, json=json) def delete(self, endpoint: str, params=None): """ @@ -130,8 +132,8 @@ def delete(self, endpoint: str, params=None): Returns: requests.Response: The response object from the HTTP DELETE request. """ - return self._make_request("DELETE", endpoint, params=params) - + return self._make_request('DELETE', endpoint, params=params) + def patch(self, endpoint: str, data=None, json=None): """ Make a PATCH request to the API. @@ -143,4 +145,4 @@ def patch(self, endpoint: str, data=None, json=None): Returns: requests.Response: The response object from the HTTP PATCH request. """ - return self._make_request("PATCH", endpoint, data=data, json=json) + return self._make_request('PATCH', endpoint, data=data, json=json) diff --git a/hub_sdk/base/auth.py b/hub_sdk/base/auth.py index 62bcb05..9aa8fcb 100644 --- a/hub_sdk/base/auth.py +++ b/hub_sdk/base/auth.py @@ -1,13 +1,16 @@ from distutils.sysconfig import PREFIX -from hub_sdk.helpers.logger import logger + import requests + from hub_sdk.config import FIREBASE_AUTH_URL, HUB_API_ROOT, HUB_WEB_ROOT +from hub_sdk.helpers.logger import logger + class Auth: + def __init__(self): self.get_auth_header = None - def authenticate(self) -> bool: """ Attempt to authenticate with the server using either id_token or API key. @@ -18,16 +21,16 @@ def authenticate(self) -> bool: try: header = self.get_auth_header() if header: - r = requests.post(f"{HUB_API_ROOT}/v1/auth", headers=header) - if not r.json().get("success", False): - raise ConnectionError("Unable to authenticate.") + r = requests.post(f'{HUB_API_ROOT}/v1/auth', headers=header) + if not r.json().get('success', False): + raise ConnectionError('Unable to authenticate.') return True - raise ConnectionError("User has not authenticated locally.") + raise ConnectionError('User has not authenticated locally.') except ConnectionError: self.id_token = self.api_key = False # reset invalid - logger.warning(f"{PREFIX} Invalid API key ⚠️") + logger.warning(f'{PREFIX} Invalid API key ⚠️') return False - + def get_auth_header(self): """ Get the authentication header for making API requests. @@ -60,7 +63,6 @@ def set_api_key(self, key: str): """ self.api_key = key - def authorize(self, email: str, password: str) -> bool: """ Authorize the user by obtaining an idToken through a POST request with email and password. @@ -73,13 +75,13 @@ def authorize(self, email: str, password: str) -> bool: bool: True if authorization is successful, False otherwise. """ try: - headers = {"origin": HUB_WEB_ROOT} - response = requests.post(FIREBASE_AUTH_URL, json={"email": email, "password": password}, headers=headers) + headers = {'origin': HUB_WEB_ROOT} + response = requests.post(FIREBASE_AUTH_URL, json={'email': email, 'password': password}, headers=headers) if response.status_code == 200: - self.id_token = response.json().get("idToken") + self.id_token = response.json().get('idToken') return True else: - raise ConnectionError("Authorization failed.") + raise ConnectionError('Authorization failed.') except ConnectionError: - logger.warning(f"{PREFIX} Invalid API key ⚠️") - return False \ No newline at end of file + logger.warning(f'{PREFIX} Invalid API key ⚠️') + return False diff --git a/hub_sdk/base/crud_client.py b/hub_sdk/base/crud_client.py index f233e25..3a78053 100644 --- a/hub_sdk/base/crud_client.py +++ b/hub_sdk/base/crud_client.py @@ -1,9 +1,10 @@ -from hub_sdk.helpers.logger import logger from hub_sdk.base.api_client import APIClient from hub_sdk.config import HUB_FUNCTIONS_ROOT +from hub_sdk.helpers.logger import logger class CRUDClient(APIClient): + def __init__(self, base_endpoint, name, headers): """ Initialize a CRUDClient instance. @@ -16,7 +17,7 @@ def __init__(self, base_endpoint, name, headers): Returns: None """ - super().__init__(f"{HUB_FUNCTIONS_ROOT}/v1/{base_endpoint}", headers) + super().__init__(f'{HUB_FUNCTIONS_ROOT}/v1/{base_endpoint}', headers) self.name = name self.logger = logger @@ -31,9 +32,9 @@ def create(self, data: dict) -> dict: dict or None: Created entity data if successful, None on failure. """ try: - return self.post("", json=data) + return self.post('', json=data) except Exception as e: - self.logger.error(f"Failed to create {self.name}: %s", e) + self.logger.error(f'Failed to create {self.name}: %s', e) def read(self, id: str) -> dict: """ @@ -46,9 +47,9 @@ def read(self, id: str) -> dict: dict or None: Entity details if successful, None on failure. """ try: - return self.get(f"/{id}") + return self.get(f'/{id}') except Exception as e: - self.logger.error(f"Failed to read {self.name}: %s", e) + self.logger.error(f'Failed to read {self.name}: %s', e) def update(self, id: str, data: dict) -> dict: """ @@ -62,11 +63,11 @@ def update(self, id: str, data: dict) -> dict: dict or None: Updated entity data if successful, None on failure. """ try: - return self.patch(f"/{id}", json=data) + return self.patch(f'/{id}', json=data) except Exception as e: - self.logger.error(f"Failed to update {self.name}: %s", e) + self.logger.error(f'Failed to update {self.name}: %s', e) - def delete(self, id: str, hard: bool=False) -> dict: + def delete(self, id: str, hard: bool = False) -> dict: """ Delete an entity using the API. @@ -79,9 +80,9 @@ def delete(self, id: str, hard: bool=False) -> dict: dict or None: Deleted entity data if successful, None on failure. """ try: - return super().delete(f"/{id}", hard) + return super().delete(f'/{id}', hard) except Exception as e: - self.logger.error(f"Failed to delete {self.name}: %s", e) + self.logger.error(f'Failed to delete {self.name}: %s', e) def list(self, page: int = 0, limit: int = 10) -> dict: """ @@ -95,7 +96,7 @@ def list(self, page: int = 0, limit: int = 10) -> dict: dict or None: List of entities if successful, None on failure. """ try: - params = {"page": page, "limit": limit} - return self.get("", params=params) + params = {'page': page, 'limit': limit} + return self.get('', params=params) except Exception as e: - self.logger.error(f"Failed to list {self.name}: %s", e) + self.logger.error(f'Failed to list {self.name}: %s', e) diff --git a/hub_sdk/base/paginated_list.py b/hub_sdk/base/paginated_list.py index 3f137ef..18f1257 100644 --- a/hub_sdk/base/paginated_list.py +++ b/hub_sdk/base/paginated_list.py @@ -1,8 +1,9 @@ -from hub_sdk.config import HUB_FUNCTIONS_ROOT from hub_sdk.base.api_client import APIClient +from hub_sdk.config import HUB_FUNCTIONS_ROOT class PaginatedList(APIClient): + def __init__(self, base_endpoint, name, page_size=None, headers=None): """ Initialize a PaginatedList instance. @@ -13,7 +14,7 @@ def __init__(self, base_endpoint, name, page_size=None, headers=None): page_size (int, optional): The number of items per page. Defaults to None. headers (dict, optional): Additional headers to include in API requests. Defaults to None. """ - super().__init__(f"{HUB_FUNCTIONS_ROOT}/v1/{base_endpoint}", headers) + super().__init__(f'{HUB_FUNCTIONS_ROOT}/v1/{base_endpoint}', headers) self.name = name self.page_size = page_size self.pages = [None] @@ -31,7 +32,8 @@ def _get(self, query=None): try: last_record = self.pages[self.current_page] resp = self.list( - self.page_size, last_record, + self.page_size, + last_record, query=query, ) self.__update_data(resp) @@ -40,9 +42,7 @@ def _get(self, query=None): self.logger.error('Failed to get data: %s', e) def previous(self) -> None: - """ - Move to the previous page of results if available. - """ + """Move to the previous page of results if available.""" try: if self.current_page > 0: self.current_page -= 1 @@ -51,11 +51,9 @@ def previous(self) -> None: self.logger.error('Failed to get previous page: %s', e) def next(self) -> None: - """ - Move to the next page of results if available. - """ + """Move to the next page of results if available.""" try: - if self.current_page < self.total_pages - 1: + if self.current_page < self.total_pages - 1: self.current_page += 1 self._get() except Exception as e: @@ -68,10 +66,10 @@ def __update_data(self, resp) -> None: Args: resp (dict): API response data. """ - resp_data = resp.json().get("data",{}) - self.results = resp_data.get("results",{}) - self.total_pages = resp_data.get("total") // self.page_size - last_record_id = resp_data.get("lastRecordId") + resp_data = resp.json().get('data', {}) + self.results = resp_data.get('results', {}) + self.total_pages = resp_data.get('total') // self.page_size + last_record_id = resp_data.get('lastRecordId') if last_record_id is not None: if len(self.pages) <= self.current_page + 1: self.pages.append(last_record_id) @@ -80,7 +78,7 @@ def __update_data(self, resp) -> None: else: self.pages[self.current_page + 1:] = [None] * (len(self.pages) - self.current_page - 1) - def list(self, page_size: int=10, last_record=None, query=None) -> dict: + def list(self, page_size: int = 10, last_record=None, query=None) -> dict: """ Retrieve a list of items from the API. @@ -93,11 +91,11 @@ def list(self, page_size: int=10, last_record=None, query=None) -> dict: dict: API response data. """ try: - params = {"perPage": page_size} + params = {'perPage': page_size} if last_record: - params["lastRecordId"] = last_record + params['lastRecordId'] = last_record if query: - params["query"] = query - return self.get("", params=params) + params['query'] = query + return self.get('', params=params) except Exception as e: - self.logger.error(f"Failed to list {self.name}: %s", e) + self.logger.error(f'Failed to list {self.name}: %s', e) diff --git a/hub_sdk/base/server_clients.py b/hub_sdk/base/server_clients.py index d1c4229..7728fd6 100644 --- a/hub_sdk/base/server_clients.py +++ b/hub_sdk/base/server_clients.py @@ -1,24 +1,29 @@ import os import platform +import signal import sys -from time import sleep from pathlib import Path -from hub_sdk.config import HUB_API_ROOT +from time import sleep + from hub_sdk.base.api_client import APIClient +from hub_sdk.config import HUB_API_ROOT from hub_sdk.helpers.utils import threaded -import signal def is_colab(): return 'google.colab' in platform.sys.modules + + __version__ = sys.version.split()[0] AGENT_NAME = f'python-{__version__}-colab' if is_colab() else f'python-{__version__}-local' + class ModelUpload(APIClient): + def __init__(self, headers): - super().__init__(f"{HUB_API_ROOT}/v1/models", headers) - self.name = "model" + super().__init__(f'{HUB_API_ROOT}/v1/models', headers) + self.name = 'model' self.alive = True self.agent_id = None self.rate_limits = {'metrics': 3.0, 'ckpt': 900.0, 'heartbeat': 300.0} @@ -36,11 +41,11 @@ def upload_model(self, id, epoch, weights, is_best=False, map=0.0, final=False): """ try: base_path = os.getcwd() - if Path(f"{base_path}/{weights}").is_file(): + if Path(f'{base_path}/{weights}').is_file(): with open(weights, 'rb') as f: file = f.read() - endpoint = f"/{id}/upload" + endpoint = f'/{id}/upload' data = {'epoch': epoch} if final: data.update({'type': 'final', 'map': map}) @@ -49,11 +54,11 @@ def upload_model(self, id, epoch, weights, is_best=False, map=0.0, final=False): data.update({'type': 'epoch', 'isBest': bool(is_best)}) files = {'last.pt': file} r = self.post(endpoint, data=data, files=files) - msg = "Model optimized weights uploaded." if final else "Model checkpoint weights uploaded." + msg = 'Model optimized weights uploaded.' if final else 'Model checkpoint weights uploaded.' self.logger.debug(msg) return r except Exception as e: - self.logger.error(f"Failed to upload file for {self.name}: %s", e) + self.logger.error(f'Failed to upload file for {self.name}: %s', e) raise e def upload_metrics(self, id: str, data: dict): @@ -69,12 +74,12 @@ def upload_metrics(self, id: str, data: dict): """ try: payload = {'metrics': data, 'type': 'metrics'} - endpoint = f"{HUB_API_ROOT}/v1/models/{id}" + endpoint = f'{HUB_API_ROOT}/v1/models/{id}' r = self.post(endpoint, json=payload) - self.logger.debug(f'Model metrics uploaded.') + self.logger.debug('Model metrics uploaded.') return r except Exception as e: - self.logger.error(f"Failed to upload file for {self.name}: %s", e) + self.logger.error(f'Failed to upload file for {self.name}: %s', e) raise e def export(self, id, format): @@ -90,12 +95,12 @@ def export(self, id, format): """ try: payload = {'format': format} - endpoint = f"/{id}/export" + endpoint = f'/{id}/export' return self.post(endpoint, json=payload) except Exception as e: - self.logger.error(f"Failed to export file for {self.name}: %s", e) + self.logger.error(f'Failed to export file for {self.name}: %s', e) raise e - + @threaded def _start_heartbeats(self, model_id: str, interval: dict): """ @@ -110,7 +115,6 @@ def _start_heartbeats(self, model_id: str, interval: dict): Returns: None - """ endpoint = f'{HUB_API_ROOT}/v1/agent/heartbeat/models/{model_id}' try: @@ -118,10 +122,9 @@ def _start_heartbeats(self, model_id: str, interval: dict): while self.alive: payload = { 'agent': AGENT_NAME, - 'agentId': self.agent_id, - } + 'agentId': self.agent_id, } res = self.post(endpoint, json=payload).json() - new_agent_id = res.get("data",{}).get("agentId") + new_agent_id = res.get('data', {}).get('agentId') self.logger.debug('Heartbeat sent.') @@ -131,7 +134,7 @@ def _start_heartbeats(self, model_id: str, interval: dict): self.agent_id = new_agent_id sleep(interval) except Exception as e: - self.logger.error(f"Failed to start heartbeats: {e}") + self.logger.error(f'Failed to start heartbeats: {e}') raise e def _stop_heartbeats(self) -> None: @@ -143,19 +146,19 @@ def _stop_heartbeats(self) -> None: Returns: None - """ self.alive = False self.logger.debug('Heartbeats stopped.') def _register_signal_handlers(self) -> None: """Register signal handlers for SIGTERM and SIGINT signals to gracefully handle termination.""" - signal.signal(signal.SIGTERM, self._handle_signal) # Polite request to terminate - signal.signal(signal.SIGINT, self._handle_signal) # CTRL + C + signal.signal(signal.SIGTERM, self._handle_signal) # Polite request to terminate + signal.signal(signal.SIGINT, self._handle_signal) # CTRL + C def _handle_signal(self, signum, frame) -> None: """ Handle kill signals and prevent heartbeats from being sent on Colab after termination. + This method does not use frame, it is included as it is passed by signal. """ self.logger.debug('Kill signal received!') @@ -169,41 +172,43 @@ def predict(self, id, image, config): :param id: The identifier for the prediction. :param image: The path to the image file. :param config: A configuration for the prediction (JSON). - :return: The prediction result (response from self.post). """ try: base_path = os.getcwd() image_path = os.path.join(base_path, image) - + if not os.path.isfile(image_path): - raise FileNotFoundError(f"Image file not found: {image_path}") + raise FileNotFoundError(f'Image file not found: {image_path}') - with open(image_path, "rb") as f: + with open(image_path, 'rb') as f: image_file = f.read() - files = {"image": image_file} + files = {'image': image_file} endpoint = f'{HUB_API_ROOT}/v1/predict/{id}' return self.post(endpoint, files=files, data=config) except Exception as e: - self.logger.error(f"Failed to predict for {self.name}: %s", e) + self.logger.error(f'Failed to predict for {self.name}: %s', e) raise e class ProjectUpload(APIClient): + def __init__(self, headers): """ Initialize the class with the specified headers. + Args: headers: The headers to use for API requests. """ - super().__init__(f"{HUB_API_ROOT}/v1/projects", headers) - self.name = "project" + super().__init__(f'{HUB_API_ROOT}/v1/projects', headers) + self.name = 'project' def upload_image(self, id: str, file): """ Upload a project file to the hub. + Args: id (YourIdType): The ID of the dataset to upload. file (str): The path to the dataset file to upload. @@ -214,20 +219,21 @@ def upload_image(self, id: str, file): file_path = os.path.join(base_path, file) file_name = os.path.basename(file_path) - with open(file_path, "rb") as image_file: + with open(file_path, 'rb') as image_file: project_image = image_file.read() try: files = {'file': (file_name, project_image)} - endpoint = f"/{id}/upload" + endpoint = f'/{id}/upload' r = self.post(endpoint, files=files) - self.logger.debug("Project Image uploaded successfully.") + self.logger.debug('Project Image uploaded successfully.') return r except Exception as e: - self.logger.error("Failed to upload image for %s: %s", self.name, str(e)) + self.logger.error('Failed to upload image for %s: %s', self.name, str(e)) raise e class DatasetUpload(APIClient): + def __init__(self, headers): """ Initialize the class with the specified headers. @@ -235,8 +241,8 @@ def __init__(self, headers): Args: headers: The headers to use for API requests. """ - super().__init__(f"{HUB_API_ROOT}/v1/datasets", headers) - self.name = "dataset" + super().__init__(f'{HUB_API_ROOT}/v1/datasets', headers) + self.name = 'dataset' def upload_dataset(self, id, file): """ @@ -251,14 +257,14 @@ def upload_dataset(self, id, file): """ try: base_path = os.getcwd() - if Path(f"{base_path}/{file}").is_file(): - with open(file, "rb") as f: + if Path(f'{base_path}/{file}').is_file(): + with open(file, 'rb') as f: dataset_file = f.read() - endpoint = f"/{id}/upload" + endpoint = f'/{id}/upload' files = {file: dataset_file} r = self.post(endpoint, files=files) - self.logger.debug("Dataset uploaded successfully.") + self.logger.debug('Dataset uploaded successfully.') return r except Exception as e: - self.logger.error(f"Failed to upload dataset for {self.name}: %s", e) + self.logger.error(f'Failed to upload dataset for {self.name}: %s', e) raise e diff --git a/hub_sdk/config.py b/hub_sdk/config.py index 56e5ddc..762c1b2 100644 --- a/hub_sdk/config.py +++ b/hub_sdk/config.py @@ -2,7 +2,10 @@ HUB_API_ROOT = os.environ.get('ULTRALYTICS_HUB_API', 'https://api.ultralytics.com') HUB_WEB_ROOT = os.environ.get('ULTRALYTICS_HUB_WEB', 'https://hub.ultralytics.com') -FIREBASE_AUTH_URL = os.environ.get('ULTRALYTICS_FIREBASE_AUTH_URL', 'http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyDlTep-ubgWoafviJJneFL35raoJjWFnOw') -HUB_FUNCTIONS_ROOT = f"{HUB_API_ROOT}/rest" +FIREBASE_AUTH_URL = os.environ.get( + 'ULTRALYTICS_FIREBASE_AUTH_URL', + 'http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyDlTep-ubgWoafviJJneFL35raoJjWFnOw' +) +HUB_FUNCTIONS_ROOT = f'{HUB_API_ROOT}/rest' -HUB_EXCEPTIONS = (os.environ.get('ULTRALYTICS_HUB_EXCEPTIONS', "True")).lower() == "true" +HUB_EXCEPTIONS = (os.environ.get('ULTRALYTICS_HUB_EXCEPTIONS', 'True')).lower() == 'true' diff --git a/hub_sdk/helpers/error_handler.py b/hub_sdk/helpers/error_handler.py index 8fe5825..ce02098 100644 --- a/hub_sdk/helpers/error_handler.py +++ b/hub_sdk/helpers/error_handler.py @@ -1,6 +1,8 @@ import http.client + class ErrorHandler: + def __init__(self, status_code, message=None): """ Initialize the ErrorHandler object with a given status code. @@ -19,8 +21,7 @@ def handle(self) -> str: error_handlers = { 401: self.handle_unauthorized, 404: self.handle_not_found, - 500: self.handle_internal_server_error, - } + 500: self.handle_internal_server_error, } handler = error_handlers.get(self.status_code, self.get_default_message) return handler() @@ -31,14 +32,15 @@ def handle_unauthorized(self) -> str: :return: An error message indicating unauthorized access. """ - return "Unauthorized: Please check your credentials." + return 'Unauthorized: Please check your credentials.' + def handle_not_found(self) -> str: """ Handle a resource not found error (HTTP 404). :return: An error message indicating that the requested resource was not found. """ - return "Resource not found." + return 'Resource not found.' def handle_internal_server_error(self) -> str: """ @@ -46,7 +48,7 @@ def handle_internal_server_error(self) -> str: :return: An error message indicating an internal server error. """ - return "Internal server error." + return 'Internal server error.' def handle_unknown_error(self) -> str: """ @@ -54,7 +56,7 @@ def handle_unknown_error(self) -> str: :return: An error message indicating that an unknown error occurred. """ - return "Unknown error occurred." + return 'Unknown error occurred.' def get_default_message(self) -> str: """ @@ -66,6 +68,5 @@ def get_default_message(self) -> str: Returns: str: The default error message associated with the provided status code. If no message is found, it falls back to handling an unknown error. - """ return http.client.responses.get(self.status_code, self.handle_unknown_error()) diff --git a/hub_sdk/helpers/exceptions.py b/hub_sdk/helpers/exceptions.py index 5979f49..02e9f41 100644 --- a/hub_sdk/helpers/exceptions.py +++ b/hub_sdk/helpers/exceptions.py @@ -1,5 +1,6 @@ from hub_sdk.config import HUB_EXCEPTIONS + def suppress_exceptions() -> None: """ Suppress exceptions locally based on the global HUB_EXCEPTIONS flag. @@ -24,4 +25,4 @@ def suppress_exceptions() -> None: to control exception handling behavior across multiple parts of the codebase. """ if not HUB_EXCEPTIONS: - raise \ No newline at end of file + raise diff --git a/hub_sdk/helpers/logger.py b/hub_sdk/helpers/logger.py index b5a177e..eeff119 100644 --- a/hub_sdk/helpers/logger.py +++ b/hub_sdk/helpers/logger.py @@ -3,6 +3,7 @@ class Logger: + def __init__(self, logger_name=None, log_format=None, log_level=None): """ Initialize a Logger instance. @@ -14,7 +15,8 @@ def __init__(self, logger_name=None, log_format=None, log_level=None): log_level (str): Log level for the logger. Defaults to the value of 'LOGGER_LEVEL' environment variable or 'INFO'. """ - self.log_format = log_format or os.environ.get('LOGGER_FORMAT', '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + self.log_format = log_format or os.environ.get('LOGGER_FORMAT', + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') self.log_level = log_level or os.environ.get('LOGGER_LEVEL', 'INFO') self.logger_name = logger_name or __name__ @@ -47,4 +49,5 @@ def get_logger(self) -> None: """ return self.logger -logger = Logger().get_logger() \ No newline at end of file + +logger = Logger().get_logger() diff --git a/hub_sdk/helpers/utils.py b/hub_sdk/helpers/utils.py index fb37204..88807f6 100644 --- a/hub_sdk/helpers/utils.py +++ b/hub_sdk/helpers/utils.py @@ -2,11 +2,16 @@ def threaded(func): - """Multi-threads a target function and returns thread. Usage: @threaded decorator.""" + """ + Multi-threads a target function and returns thread. + + Usage: @threaded decorator. + """ def wrapper(*args, **kwargs): """Multi-threads a given function and returns the thread.""" thread = threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True) thread.start() return thread + return wrapper diff --git a/hub_sdk/hub_client.py b/hub_sdk/hub_client.py index 871a18d..0830f9a 100644 --- a/hub_sdk/hub_client.py +++ b/hub_sdk/hub_client.py @@ -1,14 +1,14 @@ +import os + from hub_sdk.base.auth import Auth +from hub_sdk.modules.datasets import DatasetList, Datasets from hub_sdk.modules.models import ModelList, Models -from hub_sdk.modules.datasets import Datasets, DatasetList -from hub_sdk.modules.teams import Teams, TeamList -from hub_sdk.modules.projects import Projects, ProjectList -import os +from hub_sdk.modules.projects import ProjectList, Projects + def require_authentication(func): """ - A decorator function to ensure that the wrapped method can only be executed - if the client is authenticated. + A decorator function to ensure that the wrapped method can only be executed if the client is authenticated. Args: func (callable): The method to be wrapped. @@ -16,18 +16,19 @@ def require_authentication(func): Returns: callable: The wrapped method. """ + def wrapper(self, *args, **kwargs): - if not self.authenticated and not kwargs.get("public"): - raise PermissionError("Access Denied: Authentication required.") + if not self.authenticated and not kwargs.get('public'): + raise PermissionError('Access Denied: Authentication required.') return func(self, *args, **kwargs) + return wrapper - + class HUBClient(Auth): """ A client class for interacting with a HUB service, extending authentication capabilities. - Args: credentials (dict): A dictionary containing authentication credentials. Defaults to None. If None, the client will attempt @@ -40,7 +41,7 @@ class HUBClient(Auth): api_key (str): The API key for authentication. id_token (str): The identity token for authentication. """ - + def __init__(self, credentials=None): """ Initializes the HUBClient instance. @@ -50,8 +51,8 @@ def __init__(self, credentials=None): """ self.authenticated = False if not credentials: - self.api_key = os.environ.get("HUB_API_KEY") # Safely retrieve the API key from an environment variable. - credentials = {"api_key": self.api_key} + self.api_key = os.environ.get('HUB_API_KEY') # Safely retrieve the API key from an environment variable. + credentials = {'api_key': self.api_key} self.login(**credentials) @@ -73,7 +74,7 @@ def login(self, api_key=None, id_token=None, email=None, password=None): elif email and password: if self.authorize(email, password): - self.authenticated = True + self.authenticated = True @require_authentication def model(self, model_id: str = None): @@ -85,7 +86,6 @@ def model(self, model_id: str = None): """ return Models(model_id, self.get_auth_header()) - @require_authentication def dataset(self, dataset_id: str = None): """ @@ -98,7 +98,7 @@ def dataset(self, dataset_id: str = None): @require_authentication def team(self, arg): - raise Exception("Comming Soon") + raise Exception('Comming Soon') @require_authentication def project(self, project_id: str = None): @@ -110,22 +110,20 @@ def project(self, project_id: str = None): """ return Projects(project_id, self.get_auth_header()) - @require_authentication - def model_list(self , page_size: int = None, public: bool = None): + def model_list(self, page_size: int = None, public: bool = None): """ Returns a ModelList instance for interacting with a list of models. Args: page_size (int, optional): The number of models per page. Defaults to None. - public (bool, optional): + public (bool, optional): Returns: ModelList: An instance of the ModelList class. """ return ModelList(page_size, public, self.get_auth_header()) - @require_authentication def project_list(self, page_size: int = None, public: bool = None): """ @@ -138,7 +136,7 @@ def project_list(self, page_size: int = None, public: bool = None): ProjectList: An instance of the ProjectList class. """ return ProjectList(page_size, public, self.get_auth_header()) - + @require_authentication def dataset_list(self, page_size: int = None, public: bool = None): """ @@ -151,7 +149,7 @@ def dataset_list(self, page_size: int = None, public: bool = None): DatasetList: An instance of the DatasetList class. """ return DatasetList(page_size, public, self.get_auth_header()) - + @require_authentication def team_list(self, page_size=None, public=None): - raise Exception("Comming Soon") + raise Exception('Comming Soon') diff --git a/hub_sdk/modules/datasets.py b/hub_sdk/modules/datasets.py index 78aaf05..64caec4 100644 --- a/hub_sdk/modules/datasets.py +++ b/hub_sdk/modules/datasets.py @@ -1,7 +1,7 @@ from hub_sdk.base.crud_client import CRUDClient from hub_sdk.base.paginated_list import PaginatedList -from hub_sdk.config import HUB_FUNCTIONS_ROOT from hub_sdk.base.server_clients import DatasetUpload +from hub_sdk.config import HUB_FUNCTIONS_ROOT class Datasets(CRUDClient): @@ -17,7 +17,6 @@ class Datasets(CRUDClient): base_endpoint (str): The base endpoint for dataset-related API operations. item_name (str): The singular name of the dataset resource. headers (dict): Headers to include in HTTP requests. - """ def __init__(self, dataset_id=None, headers=None): @@ -28,14 +27,14 @@ def __init__(self, dataset_id=None, headers=None): arg (str or dict): Either an ID (string) or data (dictionary) for the dataset. headers (dict, optional): Headers to include in HTTP requests. Defaults to None. """ - super().__init__("datasets", "dataset", headers) + super().__init__('datasets', 'dataset', headers) self.hub_client = DatasetUpload(headers) self.id = dataset_id self.data = {} if dataset_id: self.get_data() - def get_data(self) -> None : + def get_data(self) -> None: """ Retrieves data for the current dataset instance. @@ -50,7 +49,7 @@ def get_data(self) -> None : """ if (self.id): resp = super().read(self.id).json() - self.data = resp.get("data", {}) + self.data = resp.get('data', {}) self.logger.debug('Dataset id is %s', self.id) else: self.logger.error('No dataset id has been set. Update the dataset id or create a dataset.') @@ -66,7 +65,7 @@ def create_dataset(self, dataset_data: dict): None """ resp = super().create(dataset_data).json() - self.id = resp.get("data", {}).get('id') + self.id = resp.get('data', {}).get('id') self.get_data() def delete(self, hard: bool = False): @@ -80,7 +79,7 @@ def delete(self, hard: bool = False): dict: The response from the delete request if successful, None otherwise. """ return super().delete(self.id, hard) - + def update(self, data: dict) -> dict: """ Update the dataset using its ID. @@ -107,7 +106,7 @@ def cleanup(self, id: str): Exception: If the delete request fails for any reason. """ try: - return self.delete(f"/{id}") + return self.delete(f'/{id}') except Exception as e: self.logger.error('Failed to cleanup: %s', e) @@ -126,23 +125,24 @@ def upload_dataset(self, file: str = None) -> bool: def get_download_link(self, type: str) -> str: """ - get dataset download link. + Get dataset download link. Args: type (str): """ try: - payload = {'collection': "datasets", "docId" : self.id ,'object': type} - endpoint = f"{HUB_FUNCTIONS_ROOT}/v1/storage" + payload = {'collection': 'datasets', 'docId': self.id, 'object': type} + endpoint = f'{HUB_FUNCTIONS_ROOT}/v1/storage' response = self.post(endpoint, json=payload) json = response.json() - return json.get("data",{}).get("url") + return json.get('data', {}).get('url') except Exception as e: - self.logger.error(f"Failed to download file file for {self.name}: %s", e) + self.logger.error(f'Failed to download file file for {self.name}: %s', e) raise e class DatasetList(PaginatedList): + def __init__(self, page_size=None, public=None, headers=None): """ Initialize a Dataset instance. @@ -152,7 +152,7 @@ def __init__(self, page_size=None, public=None, headers=None): public (bool, optional): Whether the items should be publicly accessible. Defaults to None. headers (dict, optional): Headers to be included in API requests. Defaults to None. """ - base_endpoint = "datasets" + base_endpoint = 'datasets' if public: - base_endpoint = f"public/{base_endpoint}" - super().__init__(base_endpoint, "dataset", page_size, headers) + base_endpoint = f'public/{base_endpoint}' + super().__init__(base_endpoint, 'dataset', page_size, headers) diff --git a/hub_sdk/modules/models.py b/hub_sdk/modules/models.py index abb0b9e..a767d1c 100644 --- a/hub_sdk/modules/models.py +++ b/hub_sdk/modules/models.py @@ -1,13 +1,14 @@ # Quick hack for testing import requests -from hub_sdk.config import HUB_FUNCTIONS_ROOT from hub_sdk.base.crud_client import CRUDClient from hub_sdk.base.paginated_list import PaginatedList from hub_sdk.base.server_clients import ModelUpload +from hub_sdk.config import HUB_FUNCTIONS_ROOT class Models(CRUDClient): + def __init__(self, model_id=None, headers=None): """ Initialize a Models instance. @@ -15,7 +16,7 @@ def __init__(self, model_id=None, headers=None): Args: headers (dict, optional): Headers to be included in API requests. Defaults to None. """ - super().__init__("models", "model", headers) + super().__init__('models', 'model', headers) self.hub_client = ModelUpload(headers) self.id = model_id self.data = {} @@ -38,7 +39,7 @@ def get_data(self) -> None: """ if (self.id): resp = super().read(self.id).json() - self.data = resp.get("data", {}) + self.data = resp.get('data', {}) self.logger.debug('Model id is %s', self.id) else: self.logger.error('No model id has been set. Update the model id or create a model.') @@ -54,7 +55,7 @@ def create_model(self, model_data: dict) -> None: None """ resp = super().create(model_data).json() - self.id = resp.get("data", {}).get('id') + self.id = resp.get('data', {}).get('id') self.get_data() def is_resumable(self) -> bool: @@ -110,7 +111,7 @@ def get_architecture(self) -> str: str or None: The architecture name followed by '.yaml' or None if not available. """ name = self.data.get('lineage', {}).get('architecture', {}).get('name') - return f"{name}.yaml" if name else None + return f'{name}.yaml' if name else None def get_dataset_url(self) -> str: """ @@ -120,13 +121,16 @@ def get_dataset_url(self) -> str: str or None: The URL of the dataset or None if not available. """ resp = requests.post( - f"{HUB_FUNCTIONS_ROOT}/v1/storage", - json={"collection": "models", "docId": self.id, "object": "dataset"}, + f'{HUB_FUNCTIONS_ROOT}/v1/storage', + json={ + 'collection': 'models', + 'docId': self.id, + 'object': 'dataset'}, headers=self.headers, ) - return resp.json().get("data", {}).get("url") + return resp.json().get('data', {}).get('url') - def get_weights_url(self, weight: str = "best"): + def get_weights_url(self, weight: str = 'best'): """ Get the URL of the model weights. @@ -138,15 +142,18 @@ def get_weights_url(self, weight: str = "best"): """ if weight != 'parent' or self.is_custom(): resp = requests.post( - f"{HUB_FUNCTIONS_ROOT}/v1/storage", - json={"collection": "models", "docId": self.id, "object": weight}, + f'{HUB_FUNCTIONS_ROOT}/v1/storage', + json={ + 'collection': 'models', + 'docId': self.id, + 'object': weight}, headers=self.headers, ) - return resp.json().get("data", {}).get("url") + return resp.json().get('data', {}).get('url') else: - return self.data.get("lineage", {}).get("parent", {}).get("url") + return self.data.get('lineage', {}).get('parent', {}).get('url') - def delete(self, hard: bool =False) -> dict: + def delete(self, hard: bool = False) -> dict: """ Delete the model resource represented by this instance. @@ -192,7 +199,7 @@ def cleanup(self, id: int) -> dict: Exception: If an error occurs during the deletion process. """ try: - return self.delete(f"/{id}") + return self.delete(f'/{id}') except Exception as e: self.logger.error('Failed to cleanup: %s', e) @@ -214,9 +221,7 @@ def upload_model( map (float): Mean average precision of the model. final (bool): Indicates if the model is the final model after training. """ - return self.hub_client.upload_model( - self.id, epoch, weights, is_best=is_best, map=map, final=final - ) + return self.hub_client.upload_model(self.id, epoch, weights, is_best=is_best, map=map, final=final) def upload_metrics(self, metrics: dict): """ @@ -227,25 +232,25 @@ def upload_metrics(self, metrics: dict): """ resp = self.hub_client.upload_metrics(self.id, metrics) return resp - + def get_download_link(self, type: str) -> str: """ - get model download link. + Get model download link. Args: type (str): """ try: - payload = {'collection': "models", "docId" : self.id ,'object': type} - endpoint = f"{HUB_FUNCTIONS_ROOT}/v1/storage" + payload = {'collection': 'models', 'docId': self.id, 'object': type} + endpoint = f'{HUB_FUNCTIONS_ROOT}/v1/storage' response = self.post(endpoint, json=payload) json = response.json() - return json.get("data",{}).get("url") + return json.get('data', {}).get('url') except Exception as e: - self.logger.error(f"Failed to download link for {self.name}: %s", e) + self.logger.error(f'Failed to download link for {self.name}: %s', e) raise e - def start_heartbeat(self, interval: int =60): + def start_heartbeat(self, interval: int = 60): """ Starts sending heartbeat signals to a remote hub server. @@ -280,19 +285,19 @@ def stop_heartbeat(self) -> None: def export(self, format): """ - export to Ultralytics HUB. + Export to Ultralytics HUB. - Args: + Args: export (dict): """ resp = self.hub_client.export(self.id, format) return resp - + def predict(self, image, config): """ - predict to Ultralytics HUB. + Predict to Ultralytics HUB. - Args: + Args: predict (dict): """ resp = self.hub_client.predict(self.id, image, config) @@ -300,6 +305,7 @@ def predict(self, image, config): class ModelList(PaginatedList): + def __init__(self, page_size=None, public=None, headers=None): """ Initialize a ModelList instance. @@ -309,7 +315,7 @@ def __init__(self, page_size=None, public=None, headers=None): public (bool, optional): Whether the items should be publicly accessible. Defaults to None. headers (dict, optional): Headers to be included in API requests. Defaults to None. """ - base_endpoint = "models" + base_endpoint = 'models' if public: - base_endpoint = f"public/{base_endpoint}" - super().__init__(base_endpoint, "model", page_size, headers) + base_endpoint = f'public/{base_endpoint}' + super().__init__(base_endpoint, 'model', page_size, headers) diff --git a/hub_sdk/modules/projects.py b/hub_sdk/modules/projects.py index 3908f73..bb3794f 100644 --- a/hub_sdk/modules/projects.py +++ b/hub_sdk/modules/projects.py @@ -2,7 +2,9 @@ from hub_sdk.base.paginated_list import PaginatedList from hub_sdk.base.server_clients import ProjectUpload + class Projects(CRUDClient): + def __init__(self, project_id=None, headers=None): """ Initialize a Projects object for interacting with project data via CRUD operations. @@ -12,7 +14,7 @@ def __init__(self, project_id=None, headers=None): headers (dict, optional): A dictionary of HTTP headers to be included in API requests. Defaults to None. """ - super().__init__("projects", "project", headers) + super().__init__('projects', 'project', headers) self.hub_client = ProjectUpload(headers) self.id = project_id self.data = {} @@ -34,7 +36,7 @@ def get_data(self) -> None: """ if (self.id): resp = super().read(self.id).json() - self.data = resp.get("data", {}) + self.data = resp.get('data', {}) self.logger.debug('Project id is %s', self.id) else: self.logger.error('No project id has been set. Update the project id or create a project.') @@ -50,10 +52,9 @@ def create_project(self, project_data: dict) -> None: None """ resp = super().create(project_data).json() - self.id = resp.get("data", {}).get('id') + self.id = resp.get('data', {}).get('id') self.get_data() - def delete(self, hard: bool = False): """ Delete the project. @@ -102,7 +103,7 @@ def cleanup(self, id: str): Exception: If there is an issue with the API request or response during cleanup. """ try: - return self.delete(f"/{id}") + return self.delete(f'/{id}') except Exception as e: self.logger.error('Failed to cleanup: %s', e) @@ -121,7 +122,8 @@ def upload_image(self, file): class ProjectList(PaginatedList): - def __init__(self, page_size: int =None, public: bool =None, headers : dict=None): + + def __init__(self, page_size: int = None, public: bool = None, headers: dict = None): """ Initialize a ProjectList instance. @@ -130,7 +132,7 @@ def __init__(self, page_size: int =None, public: bool =None, headers : dict=None public (bool, optional): Whether the items should be publicly accessible. Defaults to None. headers (dict, optional): Headers to be included in API requests. Defaults to None. """ - base_endpoint = "projects" + base_endpoint = 'projects' if public: - base_endpoint = f"public/{base_endpoint}" - super().__init__(base_endpoint, "project", page_size, headers) + base_endpoint = f'public/{base_endpoint}' + super().__init__(base_endpoint, 'project', page_size, headers) diff --git a/hub_sdk/modules/teams.py b/hub_sdk/modules/teams.py index 2d05cf7..b755036 100644 --- a/hub_sdk/modules/teams.py +++ b/hub_sdk/modules/teams.py @@ -5,17 +5,18 @@ class Teams(CRUDClient): """ A class representing CRUD operations for managing teams. - + This class extends the CRUDClient class to provide specific functionality for managing teams. It inherits common CRUD (Create, Read, Update, Delete) operations from the parent class. - + Args: headers (dict, optional): Headers to be included in the API requests. Attributes: entity_type (str): The type of entity being managed (e.g., "team"). """ + def __init__(self, team_id=None, headers=None): """ Initialize a Teams instance. @@ -24,7 +25,7 @@ def __init__(self, team_id=None, headers=None): arg (str or dict): Either an ID (string) or data (dictionary) for the team. headers (dict, optional): Headers to be included in the API requests. """ - super().__init__("teams", "team", headers) + super().__init__('teams', 'team', headers) self.id = team_id self.data = {} if team_id: @@ -45,7 +46,7 @@ def get_data(self): """ if (self.id): resp = super().read(self.id).json() - self.data = resp.get("data", {}) + self.data = resp.get('data', {}) self.logger.debug('Team id is %s', self.id) else: self.logger.error('No team id has been set. Update the team id or create a team.') @@ -61,7 +62,7 @@ def create_team(self, team_data): None """ resp = super().create(team_data).json() - self.id = resp.get("data", {}).get('id') + self.id = resp.get('data', {}).get('id') self.get_data() def delete(self, hard=False): @@ -105,11 +106,13 @@ def cleanup(self, id): Exception: If the delete request fails. """ try: - return self.delete(f"/{id}") + return self.delete(f'/{id}') except Exception as e: self.logger.error('Failed to cleanup: %s', e) + class TeamList(PaginatedList): + def __init__(self, page_size=None, public=None, headers=None): """ Initialize a TeamList instance. @@ -119,7 +122,7 @@ def __init__(self, page_size=None, public=None, headers=None): public (bool, optional): Whether the items should be publicly accessible. Defaults to None. headers (dict, optional): Headers to be included in API requests. Defaults to None. """ - base_endpoint = "datasets" + base_endpoint = 'datasets' if public: - base_endpoint = f"public/{base_endpoint}" - super().__init__(base_endpoint, "team", page_size, headers) + base_endpoint = f'public/{base_endpoint}' + super().__init__(base_endpoint, 'team', page_size, headers) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..fc31b5b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,69 @@ +# Project-wide configuration file, can be used for package metadata and other toll configurations +# Example usage: global configuration for PEP8 (via flake8) setting or default pytest arguments +# Local usage: pip install pre-commit, pre-commit run --all-files + +[metadata] +license_files = LICENSE +description_file = README.md + +[tool:pytest] +norecursedirs = + .git + dist + build +addopts = + --doctest-modules + --durations=30 + --color=yes + +[coverage:run] +source = hub_sdk/ +data_file = tests/.coverage + +[flake8] +max-line-length = 120 +exclude = .tox,*.egg,build,temp +select = E,W,F +doctests = True +verbose = 2 +# https://pep8.readthedocs.io/en/latest/intro.html#error-codes +format = pylint +# see: https://www.flake8rules.com/ +ignore = E731,F405,E402,W504,E501 + # E731: Do not assign a lambda expression, use a def + # F405: name may be undefined, or defined from star imports: module + # E402: module level import not at top of file + # W504: line break after binary operator + # E501: line too long + # removed: + # F401: module imported but unused + # E231: missing whitespace after β€˜,’, β€˜;’, or β€˜:’ + # E127: continuation line over-indented for visual indent + # F403: β€˜from module import *’ used; unable to detect undefined names + + +[isort] +# https://pycqa.github.io/isort/docs/configuration/options.html +line_length = 120 +# see: https://pycqa.github.io/isort/docs/configuration/multi_line_output_modes.html +multi_line_output = 0 + +[yapf] +based_on_style = pep8 +spaces_before_comment = 2 +COLUMN_LIMIT = 120 +COALESCE_BRACKETS = True +SPACES_AROUND_POWER_OPERATOR = True +SPACE_BETWEEN_ENDING_COMMA_AND_CLOSING_BRACKET = True +SPLIT_BEFORE_CLOSING_BRACKET = False +SPLIT_BEFORE_FIRST_ARGUMENT = False +# EACH_DICT_ENTRY_ON_SEPARATE_LINE = False + +[docformatter] +wrap-summaries = 120 +wrap-descriptions = 120 +in-place = true +make-summary-multi-line = false +pre-summary-newline = true +force-wrap = false +close-quotes-on-newline = true diff --git a/setup.py b/setup.py index 7bb01b8..afa3f99 100644 --- a/setup.py +++ b/setup.py @@ -1,50 +1,37 @@ -from setuptools import setup, find_packages - import re from pathlib import Path import pkg_resources as pkg +from setuptools import find_packages, setup # Settings FILE = Path(__file__).resolve() PARENT = FILE.parent # root directory -README = (PARENT / "README.md").read_text(encoding="utf-8") -REQUIREMENTS = [ - f"{x.name}{x.specifier}" - for x in pkg.parse_requirements((PARENT / "requirements.txt").read_text()) -] +README = (PARENT / 'README.md').read_text(encoding='utf-8') +REQUIREMENTS = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements((PARENT / 'requirements.txt').read_text())] def get_version(): - file = PARENT / "hub_sdk/__init__.py" - return re.search( - r'^__version__ = [\'"]([^\'"]*)[\'"]', file.read_text(encoding="utf-8"), re.M - )[1] + file = PARENT / 'hub_sdk/__init__.py' + return re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', file.read_text(encoding='utf-8'), re.M)[1] setup( - name="hub_sdk", + name='hub_sdk', version=get_version(), - python_requires=">=3.8", - author="Ultralytics", - author_email="hello@ultralytics.com", - description="Ultralytics HUB Client SDK", + python_requires='>=3.8', + author='Ultralytics', + author_email='hello@ultralytics.com', + description='Ultralytics HUB Client SDK', long_description=README, - long_description_content_type="text/markdown", - url="https://github.com/ultralytics/hub-sdk", + long_description_content_type='text/markdown', + url='https://github.com/ultralytics/hub-sdk', project_urls={ - "Bug Reports": "https://github.com/ultralytics/hub/issues", - "Funding": "https://ultralytics.com", - "Source": "https://github.com/ultralytics/hub-sdk", - }, - extras_require={ - 'dev': [ - 'mkdocs==1.5.2', - 'mkdocstrings-python==1.1.2', - 'mkdocs-material==9.2.7' - ] - }, + 'Bug Reports': 'https://github.com/ultralytics/hub/issues', + 'Funding': 'https://ultralytics.com', + 'Source': 'https://github.com/ultralytics/hub-sdk', }, + extras_require={'dev': ['mkdocs==1.5.2', 'mkdocstrings-python==1.1.2', 'mkdocs-material==9.2.7']}, packages=find_packages(), install_requires=REQUIREMENTS, - keywords="machine-learning, deep-learning, vision, ML, DL, AI, YOLO, YOLOv3, YOLOv5, YOLOv8, HUB, Ultralytics", + keywords='machine-learning, deep-learning, vision, ML, DL, AI, YOLO, YOLOv3, YOLOv5, YOLOv8, HUB, Ultralytics', ) diff --git a/tests/test.py b/tests/test.py index 477428e..e69de29 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,172 +0,0 @@ -from hub_sdk import HUBClient - -## Authenticate with the server -# crednetials = {"api_key": "99f3febd63071ad6c2d7fd17c1886cb01d8bded8ac"} -# crednetials = {"email": "rick.sanchez@citadel.com", "password": "987654321"} -# client = HUBClient(crednetials) - -################################## -## Models Operations - -# model_list = client.model_list(page_size=10, public=True) -# print(model_list.results) -# model_list.next() -# model_list.previous() - -## Project , Dataset ID for create New model -# data = {"meta": {"name": "sdk model"}, "projectId": "", "datasetId": "", "config":{"batchSize":"-1", "cache":"ram", "device":"name" , "epochs":"5", "imageSize":"640" ,"patience":"5"}} -# model = client.model() -# model.create_model(data) - -## Updata model -# model = client.model("") -# print(model.update({"meta": {"name": "model Name"}})) - -## delete model -# model = client.model("") -# model.delete() - -# modelId = "URlpJ8JjvumpwMiLElSf" # Use Model ID to get model and upload model -# model = client.model(modelId) -# print(model.data) - -# data = { -# 1: '{"loss/1": 0.5, "accuracy/1": 0.85}', -# 2: '{"loss/2": 0.4, "accuracy/2": 0.88}', -# 3: '{"loss/3": 0.3, "accuracy/3": 0.90}', -# } -# model.upload_metrics(data) # upload metrics - -## Exports -# modelId = "Epi7kTk7p2fQNHBGaPcD" # Use Model ID for export model -# model = client.model(modelId) -# print(model.data) -# model.export(format="pyTorch") # upload export - - -## Firebase-storage -# data = { -# "collection": "models", -# "docId": "Epi7kTk7p2fQNHBGaPcD", -# "object": "best" -# } -# storage = client.model(data) -# print(storage.get_dataset_url(data)) - -## Dataset Operations -## create dataset - -# Upload model - -# datasetID = "KUGRLIK8C4nytMcYNiW9" -# dataset = client.model(datasetID) -# print("dataset Data", dataset.data) -# uploadDataset = dataset.upload_model(is_best= True, epoch=5, weights="example.pt") - - -# Dataset Operations - -# Upload model -# modelID = "" -# model = client.model(datasetID) -# print("model Data", model.data) -# uploadmodel = model.upload_model(is_best= True, epoch=5, weights="example.pt") - - -# MODEL_ID = "" -# image = "im1.jpg" -# config = {"size": 640, "confidence": 0.25, "iou": 0.45} -# model = client.model(MODEL_ID) -# results = model.predict(image=image, config=config) - -################################### -## Dataset Operations -# create dataset - -# data = {"meta":{"name":"my dataset"}, "filename": ""} -# dataset = client.dataset() -# dataset.create_dataset(data) - -## get dataset by Id -# dataset = client.dataset('') -# print(dataset.data) - -## Updata dataset -# dataset = client.dataset("") -# print(dataset.update({"meta": {"name": "dataset Name"}})) - -## delete dataset -# dataset = client.dataset("") -# dataset.delete() - -## List dataset -# dataset = client.dataset_list(page_size=1) -# print(dataset.results) -# dataset.next() -# dataset.previous() - -# datasetID = "model ID" # Use Model ID to get model and upload model -# dataset = client.dataset(datasetID) -# print(dataset.data) -# print(dataset.get_download_link("archive")) - -# Upload Dataset - -# datasetID = "" -# dataset = client.dataset(datasetID) -# print("dataset Data", dataset.data) -# uploadDataset = dataset.upload_dataset(file="coco8.zip") - -##################################### -## Project Operations -## create project -# data ={"meta":{"name":"my project"}} -# project = client.project() -# project.create_project(data) - -## get project -# project = client.project('') -# print(project.data) - -## update project -# project = client.project('') -# print(project.update({"meta": {"name": "Project name update"}})) - -## delete project -# project = client.project("") -# print(project.delete()) - -## List Project -# projects = client.project_list(page_size=1, public=True) -# print(projects.results) -# projects.next() -# projects.previous() - - -# projectID = "" # Use Model ID to get model and upload model -# project = client.project(projectID) -# print(project.data) -# project.upload_image(file = "project_image.jpeg") # upload metrics - - -####################################### -## Team Operations -# team = client.team({"meta":{"name":"my team"}}) # Create Teams -# print(team.data) -# team = client.team('Teams ID') # teams ID to data -# print(team.data) -# team.update({"meta": {"name": "Team Name update"}}) # for update teams -# team.delete() # for delete teams - -# teams = client.team_list(page_size=1) # teams list -# print(teams.results) -# teams.next() -# teams.previous() - - - - -## Coming Soon - -## Cloud training -# response = models.train("MODEL_ID", {"config": "args"}) \ No newline at end of file diff --git a/tests/testCase_dataset.py b/tests/testCase_dataset.py index c03daa1..599d1af 100644 --- a/tests/testCase_dataset.py +++ b/tests/testCase_dataset.py @@ -1,59 +1,60 @@ import unittest + from hub_sdk import HUBClient class TestDatasetSDK(unittest.TestCase): def setUp(self): - self.client = HUBClient({"email": "", "password": ""}) # Add Email Password + self.client = HUBClient({'email': '', 'password': ''}) # Add Email Password self.dataset = self.client.dataset() self.assertIsNotNone(self.client) self.assertIsNotNone(self.dataset) def test_create_dataset(self): - data = {"meta":{"name":"my dataset"}, "filename": "example.pt"} + data = {'meta': {'name': 'my dataset'}, 'filename': 'example.pt'} expected_result = None result = self.dataset.create_dataset(data) self.assertEqual(result, expected_result) - self.assertEqual(result.data.get("meta").get("name"), "my dataset") + self.assertEqual(result.data.get('meta').get('name'), 'my dataset') def test_get_dataset_by_id(self): - dataset_id = "" # Add dataset ID + dataset_id = '' # Add dataset ID result = self.client.dataset(dataset_id) self.assertEqual(result.id, dataset_id) def test_update_dataset(self): - data = {"meta": {"name": "Ricks Secret Dataset"}} - project_id = "" # Add dataset ID + data = {'meta': {'name': 'Ricks Secret Dataset'}} + project_id = '' # Add dataset ID result = self.client.dataset(project_id) - expected_result = "Ricks Secret Dataset" + expected_result = 'Ricks Secret Dataset' result.update(data) - update_name = result.data.get("meta").get("name") + update_name = result.data.get('meta').get('name') self.assertEqual(update_name, expected_result) def test_list_datasets(self): - result = self.client.dataset_list(page_size = 1, public=True) + result = self.client.dataset_list(page_size=1, public=True) self.assertNotEqual(len(result), 0) def test_upload_dataset(self): - dataset_id = "" # Add dataset ID + dataset_id = '' # Add dataset ID result = self.client.dataset(dataset_id) - result.upload_dataset(file="coco8.zip") + result.upload_dataset(file='coco8.zip') self.assertEqual(result.id, dataset_id) def test_get_download_link(self): - dataset_id = "" # Add dataset ID + dataset_id = '' # Add dataset ID result = self.client.dataset(dataset_id) - download_link = result.get_download_link("archive") + download_link = result.get_download_link('archive') self.assertEqual(result.id, dataset_id) - self.assertTrue(download_link.startswith("http")) + self.assertTrue(download_link.startswith('http')) def test_delete_dataset(self): - dataset_id = "" # Add dataset ID + dataset_id = '' # Add dataset ID result = self.client.dataset(dataset_id) deleted = result.delete() expected_result = 200 - self.assertEqual(deleted.status_code , expected_result) + self.assertEqual(deleted.status_code, expected_result) if __name__ == '__main__': diff --git a/tests/testCase_model.py b/tests/testCase_model.py index aa7ddac..1d50d28 100644 --- a/tests/testCase_model.py +++ b/tests/testCase_model.py @@ -1,4 +1,5 @@ import unittest + from hub_sdk import HUBClient @@ -6,76 +7,74 @@ class TestSDK(unittest.TestCase): def setUp(self): # Mock the client and model objects - self.client = HUBClient({"email": "", "password": "Password"}) # Add Email Password + self.client = HUBClient({'email': '', 'password': 'Password'}) # Add Email Password self.model = self.client.model() self.assertIsNotNone(self.client) self.assertIsNotNone(self.model) def test_create_model(self): data = { - "meta": {"name": "sdk model"}, - "projectId": "", # Add Project ID - "datasetId": "", # Add Dataset ID - "config": { - "batchSize": "-1", - "cache": "ram", - "device": "name", - "epochs": "5", - "imageSize": "640", - "patience": "5" - } - } + 'meta': { + 'name': 'sdk model'}, + 'projectId': '', # Add Project ID + 'datasetId': '', # Add Dataset ID + 'config': { + 'batchSize': '-1', + 'cache': 'ram', + 'device': 'name', + 'epochs': '5', + 'imageSize': '640', + 'patience': '5'}} expected_result = None result = self.model.create_model(data) self.assertEqual(result, expected_result) def test_get_model_data(self): - model_id = "" # Add Model ID + model_id = '' # Add Model ID result = self.client.model(model_id) self.assertEqual(result.id, model_id) def test_upload_metrics(self): - model_id = "" # Add Model ID + model_id = '' # Add Model ID data = { 1: '{"loss/1": 0.5, "accuracy/1": 0.85}', 2: '{"loss/2": 0.4, "accuracy/2": 0.88}', - 3: '{"loss/3": 0.3, "accuracy/3": 0.90}', - } - expected_result = "" # Add Model ID + 3: '{"loss/3": 0.3, "accuracy/3": 0.90}', } + expected_result = '' # Add Model ID result = self.client.model(model_id) response = result.upload_metrics(data) - self.assertEqual(result.data.get("id"), expected_result) + self.assertEqual(result.data.get('id'), expected_result) self.assertEqual(response.status_code, 200) def test_export_model(self): - model_id = "" # Add Model ID + model_id = '' # Add Model ID result = self.client.model(model_id) - response = result.export(format="pyTorch") + response = result.export(format='pyTorch') self.assertEqual(response.status_code, 200) if response.status_code == 500: - self.assertTrue("Unhandled server error.") + self.assertTrue('Unhandled server error.') def test_get_download_link(self): - model_id = "" # Add Model ID + model_id = '' # Add Model ID result = self.client.model(model_id) - download_link = result.get_download_link("best") + download_link = result.get_download_link('best') print(download_link) self.assertEqual(result.id, model_id) - self.assertTrue(download_link.startswith("http")) + self.assertTrue(download_link.startswith('http')) def test_upload_model(self): - model_id = "" # Add Model ID - expected_result = "" + model_id = '' # Add Model ID + expected_result = '' result = self.client.model(model_id) - result.upload_model(is_best=True, epoch="5", weights="example.pt") + result.upload_model(is_best=True, epoch='5', weights='example.pt') self.assertEqual(result, expected_result) def test_delete_model(self): - model_id = "" # Add Model ID + model_id = '' # Add Model ID result = self.client.model(model_id) deleted = result.delete() expected_result = 200 - self.assertEqual(deleted.status_code , expected_result) + self.assertEqual(deleted.status_code, expected_result) if __name__ == '__main__': diff --git a/tests/testCase_project.py b/tests/testCase_project.py index 482302d..00ed1fd 100644 --- a/tests/testCase_project.py +++ b/tests/testCase_project.py @@ -1,53 +1,52 @@ import unittest + from hub_sdk import HUBClient class TestProjectSDK(unittest.TestCase): def setUp(self): - self.client = HUBClient({"email": "", "password": ""}) # Add Email Password + self.client = HUBClient({'email': '', 'password': ''}) # Add Email Password self.project = self.client.project() self.assertIsNotNone(self.client) self.assertIsNotNone(self.project) def test_create_project(self): - data = { - "meta": {"name": "my project"} - } + data = {'meta': {'name': 'my project'}} expected_result = None result = self.project.create_project(data) self.assertEqual(result, expected_result) - self.assertEqual(result.data.get("meta").get("name"), "my project") + self.assertEqual(result.data.get('meta').get('name'), 'my project') def test_get_project_by_id(self): - project_id = "" # Add Project ID + project_id = '' # Add Project ID result = self.client.project(project_id) self.assertEqual(result.id, project_id) def test_update_project(self): - data = {"meta": {"name": "Project name update"}} - project_id = "" # Add Project ID + data = {'meta': {'name': 'Project name update'}} + project_id = '' # Add Project ID result = self.client.project(project_id) - expected_result = "Project name update" + expected_result = 'Project name update' result.update(data) - self.assertEqual(result.data.get("meta").get("name") , expected_result) + self.assertEqual(result.data.get('meta').get('name'), expected_result) def test_list_projects(self): - result = self.client.project_list(page_size = 1, public=True) + result = self.client.project_list(page_size=1, public=True) self.assertNotEqual(len(result), 0) def test_upload_image(self): - project_id = "" # Add Project ID + project_id = '' # Add Project ID result = self.client.project(project_id) - result.upload_image(file="project_image.jpeg") + result.upload_image(file='project_image.jpeg') self.assertEqual(result.id, project_id) def test_delete_project(self): - project_id = "" # Add Project ID + project_id = '' # Add Project ID result = self.client.project(project_id) deleted = result.delete() expected_result = 200 - self.assertEqual(deleted.status_code , expected_result) + self.assertEqual(deleted.status_code, expected_result) if __name__ == '__main__':