diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 23529d8..a774e91 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -55,12 +55,12 @@ jobs: uses: ./actions/py/test with: python-version: ${{ matrix.python-version }} - unit-test-directory: tests/unit + unit-test-directory: tests/py/unit # TMP disabled to allow publishing to PyPi - # - name: Run integration tests] + # - name: Run integration tests # uses: ./actions/py/test # with: - # integration-test-directory: tests/integration + # integration-test-directory: tests/py/integration # env: # TEST_HOST: platform.mat3ra.com # TEST_PORT: 443 diff --git a/.gitignore b/.gitignore index 9c487cd..cca5e6a 100644 --- a/.gitignore +++ b/.gitignore @@ -95,7 +95,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. diff --git a/README.md b/README.md index c60cceb..94d8221 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![PyPI version](https://badge.fury.io/py/exabyte-api-client.svg)](https://badge.fury.io/py/exabyte-api-client) +[![PyPI version](https://badge.fury.io/py/mat3ra-api-client.svg)](https://badge.fury.io/py/mat3ra-api-client) -This package provides access to Exabyte.io [RESTful API](https://docs.mat3ra.com/rest-api/overview/). +This package provides access to Mat3ra.com [RESTful API](https://docs.mat3ra.com/rest-api/overview/). # Installation @@ -16,35 +16,81 @@ Install using pip: - from PyPI: ```bash -pip install exabyte-api-client +pip install mat3ra-api-client ``` - from source code in development mode: ```bash -git clone git@github.com:Exabyte-io/exabyte-api-client.git -cd exabyte-api-client +git clone git@github.com:Exabyte-io/api-client.git +cd api-client pip install -e . ``` +# Usage + +```python +from mat3ra.api_client import APIClient + +# Authenticate with OIDC token +client = APIClient.authenticate() + +# Access endpoints +materials = client.materials.list() +``` + # Examples -[exabyte-api-examples](https://github.com/Exabyte-io/exabyte-api-examples) repository contains examples for performing most-common tasks in the Exabyte.io platform through its RESTful API in Jupyter Notebook format. +[api-examples](https://github.com/Exabyte-io/api-examples) repository contains examples for performing most-common tasks in the Mat3ra.com platform through its RESTful API in Jupyter Notebook format. # Testing -A Virtualenv environment can be created and the tests run with the included `run-tests.sh` script. -To run the unit tests in Python 3, you can: + +The package uses pytest for testing. Tests are organized into unit and integration tests. + +## Running Tests + +### Unit Tests (No environment setup required) + +Run all unit tests: +```bash +pytest tests/py/unit ``` -./run-tests -t=unit + +### Integration Tests (Requires API credentials) + +Integration tests require the following environment variables to be set: + +- `TEST_HOST` - API host (e.g., `platform.mat3ra.com`) +- `TEST_PORT` - API port (e.g., `443`) +- `TEST_ACCOUNT_ID` - Your account ID +- `TEST_AUTH_TOKEN` - Your authentication token +- `TEST_SECURE` - Use HTTPS (optional, default: `False`) +- `TEST_VERSION` - API version (optional, default: `2018-10-01`) + +To run integration tests: +```bash +export TEST_HOST=platform.mat3ra.com +export TEST_PORT=443 +export TEST_ACCOUNT_ID=your-account-id +export TEST_AUTH_TOKEN=your-auth-token +export TEST_SECURE=true + +pytest tests/py/integration ``` -To run the integration tests in Python 2, you can: +### Run All Tests + +```bash +pytest tests/py ``` -./run-tests -p=python2 -t=integration + +### Run Tests with Coverage + +```bash +pytest tests/py/unit --cov=mat3ra.api_client --cov-report=term --cov-report=html ``` -(assuming you have a `python2` binary in your PATH environment). -Note that the integration tests require a REST API service against which the live tests will run. See `tests/integration/__init__.py` for the environment variable details. +**Note:** Integration tests will be automatically skipped if required environment variables are not set. © 2020 Exabyte Inc. diff --git a/exabyte_api_client/__init__.py b/exabyte_api_client/__init__.py deleted file mode 100644 index cd1c62f..0000000 --- a/exabyte_api_client/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# ruff: noqa: F401 -try: - from ._version import version as __version__ -except ModuleNotFoundError: - __version__ = None - -from exabyte_api_client.endpoints.bank_materials import BankMaterialEndpoints -from exabyte_api_client.endpoints.bank_workflows import BankWorkflowEndpoints -from exabyte_api_client.endpoints.jobs import JobEndpoints -from exabyte_api_client.endpoints.login import LoginEndpoint -from exabyte_api_client.endpoints.logout import LogoutEndpoint -from exabyte_api_client.endpoints.materials import MaterialEndpoints -from exabyte_api_client.endpoints.metaproperties import MetaPropertiesEndpoints -from exabyte_api_client.endpoints.projects import ProjectEndpoints -from exabyte_api_client.endpoints.properties import PropertiesEndpoints -from exabyte_api_client.endpoints.workflows import WorkflowEndpoints - -from exabyte_api_client.client import APIClient diff --git a/pyproject.toml b/pyproject.toml index d88ca30..6769ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,105 @@ +[project] +name = "mat3ra.api-client" +dynamic = ["version"] +description = "Mat3ra.com Python Client for RESTful API" +readme = "README.md" +requires-python = ">=3.10" +license = {file = "LICENSE.md"} +authors = [ + { name = "Exabyte Inc.", email = "info@mat3ra.com" } +] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development", + "License :: OSI Approved :: Apache Software License", +] +dependencies = [ + "requests>=2.26.0", + "pydantic>=2,<3", +] + +[project.optional-dependencies] +dev = [ + "pre-commit", + "black", + "ruff", + "isort", + "mypy", + "pip-tools", +] +tests = [ + "coverage[toml]>=5.3", + "pytest", + "pytest-cov", + "mock>=4.0.3", +] +all = [ + "mat3ra-api-client[tests]", + "mat3ra-api-client[dev]", +] + [build-system] -requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] +requires = [ + "setuptools>=42", + "setuptools-scm[toml]>=3.4" +] build-backend = "setuptools.build_meta" [tool.setuptools_scm] -write_to = "exabyte_api_client/_version.py" +git_describe_command = "git describe --tags --long" +write_to = "src/py/mat3ra/api_client/_version.py" + +[tool.setuptools.packages.find] +where = ["src/py"] + +[tool.black] +line-length = 120 +target-version = ['py310'] +extend-exclude = ''' +( + tests\/fixtures* +) +''' + +[tool.ruff] +extend-exclude = [ + "tests/fixtures" +] +line-length = 120 +target-version = "py310" + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true + +[tool.pytest.ini_options] +pythonpath = [ + "src/py", +] +testpaths = [ + "tests/py" +] [tool.coverage.run] -source = ['.'] -omit = ['env*/*', 'venv*/*', 'tests/*'] +source = ["src/py"] +omit = ["env*/*", "venv*/*", "tests/*", "src/py/mat3ra/api_client/_version.py"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 408212c..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -certifi==2020.12.5 -chardet==3.0.4 -coverage==5.5 -idna==2.7 -mock==4.0.3 -requests>=2.26.0 -pydantic>=2,<3 -toml==0.10.2 -urllib3==1.26.5 diff --git a/run-tests.sh b/run-tests.sh deleted file mode 100755 index 0b600bb..0000000 --- a/run-tests.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -set -e - -TEST_TYPE="unit" -PYTHON_BIN="python3" -THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" -VENV_NAME="venv" - -usage() { - echo "run-tests.sh -p=PYTHON_BIN -v=VENV_NAME -t=TEST_TYPE" - exit 1 -} - -check_args() { - for i in "$@"; do - case $i in - -t=* | --test-type=*) - TEST_TYPE="${i#*=}" - ;; - -p=* | --python-bin=*) - PYTHON_BIN="${i#*=}" - ;; - -v=* | --venvdir=*) - VENV_NAME="${i#*=}" - ;; - *) - usage - ;; - esac - done -} - -check_args $@ - -# Prepare the execution virtualenv -virtualenv --python ${PYTHON_BIN} ${THIS_DIR}/${VENV_NAME} -source ${THIS_DIR}/${VENV_NAME}/bin/activate -trap "deactivate" EXIT -if [ -f ${THIS_DIR}/requirements-dev.txt ]; then - pip install -r ${THIS_DIR}/requirements-dev.txt --no-deps -fi - -# Execute the specified test suite -coverage run -m unittest discover --verbose --catch --start-directory ${THIS_DIR}/tests/${TEST_TYPE} - -# Generate the code coverage reports -coverage report -coverage html --directory htmlcov_${TEST_TYPE} -coverage xml -o coverage_${TEST_TYPE}.xml diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a37197b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,34 +0,0 @@ -[metadata] -name = exabyte-api-client -author = Exabyte Inc. -author_email = info@mat3ra.com -description = Exabyte Python Client for RESTful API -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/Exabyte-io/api-client -classifiers = - Programming Language :: Python - Programming Language :: Python :: 3 - Development Status :: 3 - Alpha - Intended Audience :: Developers - Topic :: Software Development - License :: OSI Approved :: Apache Software License - -[options] -package_dir = - = . -packages = find: -python_requires = >= 3.6 -install_requires = - requests>=2.26.0 - -[options.extras_require] -test = - coverage[toml]>=5.3 - mock>=1.3.0 - -[options.packages.find] -where = . -exclude = - tests - tests.* diff --git a/src/py/mat3ra/__init__.py b/src/py/mat3ra/__init__.py new file mode 100644 index 0000000..efa22de --- /dev/null +++ b/src/py/mat3ra/__init__.py @@ -0,0 +1,3 @@ +"""Mat3ra namespace package.""" +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + diff --git a/src/py/mat3ra/api_client/__init__.py b/src/py/mat3ra/api_client/__init__.py new file mode 100644 index 0000000..43e090a --- /dev/null +++ b/src/py/mat3ra/api_client/__init__.py @@ -0,0 +1,18 @@ +# ruff: noqa: F401 +try: + from ._version import version as __version__ +except ModuleNotFoundError: + __version__ = None + +from mat3ra.api_client.endpoints.bank_materials import BankMaterialEndpoints +from mat3ra.api_client.endpoints.bank_workflows import BankWorkflowEndpoints +from mat3ra.api_client.endpoints.jobs import JobEndpoints +from mat3ra.api_client.endpoints.login import LoginEndpoint +from mat3ra.api_client.endpoints.logout import LogoutEndpoint +from mat3ra.api_client.endpoints.materials import MaterialEndpoints +from mat3ra.api_client.endpoints.metaproperties import MetaPropertiesEndpoints +from mat3ra.api_client.endpoints.projects import ProjectEndpoints +from mat3ra.api_client.endpoints.properties import PropertiesEndpoints +from mat3ra.api_client.endpoints.workflows import WorkflowEndpoints + +from mat3ra.api_client.client import APIClient diff --git a/exabyte_api_client/client.py b/src/py/mat3ra/api_client/client.py similarity index 78% rename from exabyte_api_client/client.py rename to src/py/mat3ra/api_client/client.py index 3d176a8..2aefd98 100644 --- a/exabyte_api_client/client.py +++ b/src/py/mat3ra/api_client/client.py @@ -4,20 +4,20 @@ import requests from pydantic import BaseModel, ConfigDict, Field -from exabyte_api_client.endpoints.bank_materials import BankMaterialEndpoints -from exabyte_api_client.endpoints.bank_workflows import BankWorkflowEndpoints -from exabyte_api_client.endpoints.jobs import JobEndpoints -from exabyte_api_client.endpoints.materials import MaterialEndpoints -from exabyte_api_client.endpoints.metaproperties import MetaPropertiesEndpoints -from exabyte_api_client.endpoints.projects import ProjectEndpoints -from exabyte_api_client.endpoints.properties import PropertiesEndpoints -from exabyte_api_client.endpoints.workflows import WorkflowEndpoints - -# Default OIDC Configuration -OIDC_BASE_URL = "http://localhost:3000/oidc" -CLIENT_ID = "default-client" -CLIENT_SECRET = "default-secret" -SCOPE = "openid profile email" +from mat3ra.api_client.endpoints.bank_materials import BankMaterialEndpoints +from mat3ra.api_client.endpoints.bank_workflows import BankWorkflowEndpoints +from mat3ra.api_client.endpoints.jobs import JobEndpoints +from mat3ra.api_client.endpoints.materials import MaterialEndpoints +from mat3ra.api_client.endpoints.metaproperties import MetaPropertiesEndpoints +from mat3ra.api_client.endpoints.projects import ProjectEndpoints +from mat3ra.api_client.endpoints.properties import PropertiesEndpoints +from mat3ra.api_client.endpoints.workflows import WorkflowEndpoints + +# Default API Configuration +DEFAULT_API_HOST = "platform.mat3ra.com" +DEFAULT_API_PORT = 443 +DEFAULT_API_VERSION = "2018-10-01" +DEFAULT_API_SECURE = True # Environment Variable Names ACCESS_TOKEN_ENV_VAR = "OIDC_ACCESS_TOKEN" @@ -28,23 +28,14 @@ ACCOUNT_ID_ENV_VAR = "ACCOUNT_ID" AUTH_TOKEN_ENV_VAR = "AUTH_TOKEN" -# Protocol Constants -PROTOCOL_HTTPS = "https" -PROTOCOL_HTTP = "http" +# Default OIDC Configuration +CLIENT_ID = "default-client" +CLIENT_SECRET = "default-secret" +SCOPE = "openid profile email" # API Paths USERS_ME_PATH = "/api/v1/users/me" -# JSON Response Keys -JSON_KEY_DATA = "data" -JSON_KEY_USER = "user" -JSON_KEY_ENTITY = "entity" -JSON_KEY_DEFAULT_ACCOUNT_ID = "defaultAccountId" - -# Error Messages -ERROR_MISSING_AUTH = "Missing auth. Provide OIDC_ACCESS_TOKEN or ACCOUNT_ID and AUTH_TOKEN." -ERROR_NO_ACCOUNT_ID_OR_TOKEN = "ACCOUNT_ID is not set and no OIDC access token is available." - class AuthContext(BaseModel): access_token: Optional[str] = None @@ -53,10 +44,10 @@ class AuthContext(BaseModel): class APIEnv(BaseModel): - host: str = Field(validation_alias=API_HOST_ENV_VAR) - port: int = Field(validation_alias=API_PORT_ENV_VAR) - version: str = Field(validation_alias=API_VERSION_ENV_VAR) - secure: bool = Field(validation_alias=API_SECURE_ENV_VAR) + host: str = Field(default=DEFAULT_API_HOST, validation_alias=API_HOST_ENV_VAR) + port: int = Field(default=DEFAULT_API_PORT, validation_alias=API_PORT_ENV_VAR) + version: str = Field(default=DEFAULT_API_VERSION, validation_alias=API_VERSION_ENV_VAR) + secure: bool = Field(default=DEFAULT_API_SECURE, validation_alias=API_SECURE_ENV_VAR) @classmethod def from_env(cls) -> "APIEnv": @@ -74,13 +65,11 @@ def from_env(cls) -> "AuthEnv": class Account(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) + client: Any = Field(exclude=True, repr=False) id_cache: Optional[str] = None - class Config: - arbitrary_types_allowed = True - validate_assignment = True - @property def id(self) -> str: if self.id_cache: @@ -89,11 +78,14 @@ def id(self) -> str: return self.id_cache -def _build_users_me_url(host: str, port: int, secure: bool) -> str: - protocol = PROTOCOL_HTTPS if secure else PROTOCOL_HTTP - port_str = f":{port}" if port not in [80, 443] else "" - return f"{protocol}://{host}{port_str}{USERS_ME_PATH}" +def _build_base_url(host: str, port: int, secure: bool, path: str) -> str: + protocol = "https" if secure else "http" + port_str = f":{port}" if port not in (80, 443) else "" + return f"{protocol}://{host}{port_str}{path}" +# Used in API-examples utils +def build_oidc_base_url(host: str, port: int, secure: bool) -> str: + return _build_base_url(host, port, secure, "/oidc") class APIClient(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow", validate_assignment=True) @@ -189,7 +181,7 @@ def _validate_auth(auth: AuthContext) -> None: return if auth.account_id and auth.auth_token: return - raise ValueError(ERROR_MISSING_AUTH) + raise ValueError("Missing auth. Provide OIDC_ACCESS_TOKEN or ACCOUNT_ID and AUTH_TOKEN.") @classmethod def authenticate( @@ -219,12 +211,12 @@ def _resolve_account_id(self) -> str: access_token = self.auth.access_token or os.environ.get(ACCESS_TOKEN_ENV_VAR) if not access_token: - raise ValueError(ERROR_NO_ACCOUNT_ID_OR_TOKEN) + raise ValueError("ACCOUNT_ID is not set and no OIDC access token is available.") - url = _build_users_me_url(self.host, self.port, self.secure) + url = _build_base_url(self.host, self.port, self.secure, USERS_ME_PATH) response = requests.get(url, headers={"Authorization": f"Bearer {access_token}"}, timeout=30) response.raise_for_status() - account_id = response.json()[JSON_KEY_DATA][JSON_KEY_USER][JSON_KEY_ENTITY][JSON_KEY_DEFAULT_ACCOUNT_ID] + account_id = response.json()["data"]["user"]["entity"]["defaultAccountId"] os.environ[ACCOUNT_ID_ENV_VAR] = account_id self.auth.account_id = account_id return account_id diff --git a/exabyte_api_client/endpoints/__init__.py b/src/py/mat3ra/api_client/endpoints/__init__.py similarity index 89% rename from exabyte_api_client/endpoints/__init__.py rename to src/py/mat3ra/api_client/endpoints/__init__.py index e3b0161..5eae989 100644 --- a/exabyte_api_client/endpoints/__init__.py +++ b/src/py/mat3ra/api_client/endpoints/__init__.py @@ -8,13 +8,13 @@ class BaseEndpoint(object): Base class for Exabyte RESTful API endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. - version (str): Exabyte API version. Defaults to 2018-10-1. + host (str): API hostname. + port (int): API port number. + version (str): API version. Defaults to 2018-10-1. secure (bool): whether to use secure http protocol (https vs http). Defaults to True. Attributes: - conn (httplib.ExabyteConnection): ExabyteConnection instance. + conn (httplib.Connection): Connection instance. """ def __init__(self, host, port, version="2018-10-1", secure=True, **kwargs): diff --git a/exabyte_api_client/endpoints/bank_entity.py b/src/py/mat3ra/api_client/endpoints/bank_entity.py similarity index 91% rename from exabyte_api_client/endpoints/bank_entity.py rename to src/py/mat3ra/api_client/endpoints/bank_entity.py index 79b7d3a..bae1573 100644 --- a/exabyte_api_client/endpoints/bank_entity.py +++ b/src/py/mat3ra/api_client/endpoints/bank_entity.py @@ -7,11 +7,11 @@ class BankEntityEndpoints(EntityEndpoint): Bank Entity endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/bank_materials.py b/src/py/mat3ra/api_client/endpoints/bank_materials.py similarity index 85% rename from exabyte_api_client/endpoints/bank_materials.py rename to src/py/mat3ra/api_client/endpoints/bank_materials.py index e77a3e7..fb53de6 100644 --- a/exabyte_api_client/endpoints/bank_materials.py +++ b/src/py/mat3ra/api_client/endpoints/bank_materials.py @@ -7,11 +7,11 @@ class BankMaterialEndpoints(BankEntityEndpoints): Bank material endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/bank_workflows.py b/src/py/mat3ra/api_client/endpoints/bank_workflows.py similarity index 85% rename from exabyte_api_client/endpoints/bank_workflows.py rename to src/py/mat3ra/api_client/endpoints/bank_workflows.py index d999257..2267665 100644 --- a/exabyte_api_client/endpoints/bank_workflows.py +++ b/src/py/mat3ra/api_client/endpoints/bank_workflows.py @@ -7,11 +7,11 @@ class BankWorkflowEndpoints(BankEntityEndpoints): Bank workflow endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/charges.py b/src/py/mat3ra/api_client/endpoints/charges.py similarity index 90% rename from exabyte_api_client/endpoints/charges.py rename to src/py/mat3ra/api_client/endpoints/charges.py index 3042417..7181cbb 100644 --- a/exabyte_api_client/endpoints/charges.py +++ b/src/py/mat3ra/api_client/endpoints/charges.py @@ -7,11 +7,11 @@ class ChargeEndpoints(EntityEndpoint): Charge endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/entity.py b/src/py/mat3ra/api_client/endpoints/entity.py similarity index 95% rename from exabyte_api_client/endpoints/entity.py rename to src/py/mat3ra/api_client/endpoints/entity.py index 05d8c07..be0ab1e 100644 --- a/exabyte_api_client/endpoints/entity.py +++ b/src/py/mat3ra/api_client/endpoints/entity.py @@ -9,11 +9,11 @@ class EntityEndpoint(BaseEndpoint): Exabyte Entity endpoint. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/enums.py b/src/py/mat3ra/api_client/endpoints/enums.py similarity index 100% rename from exabyte_api_client/endpoints/enums.py rename to src/py/mat3ra/api_client/endpoints/enums.py diff --git a/exabyte_api_client/endpoints/jobs.py b/src/py/mat3ra/api_client/endpoints/jobs.py similarity index 97% rename from exabyte_api_client/endpoints/jobs.py rename to src/py/mat3ra/api_client/endpoints/jobs.py index 1112d00..d5b703c 100644 --- a/exabyte_api_client/endpoints/jobs.py +++ b/src/py/mat3ra/api_client/endpoints/jobs.py @@ -10,11 +10,11 @@ class JobEndpoints(EntitySetEndpointsMixin, EntityEndpoint): Job endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/login.py b/src/py/mat3ra/api_client/endpoints/login.py similarity index 87% rename from exabyte_api_client/endpoints/login.py rename to src/py/mat3ra/api_client/endpoints/login.py index 1e5050c..bd92023 100644 --- a/exabyte_api_client/endpoints/login.py +++ b/src/py/mat3ra/api_client/endpoints/login.py @@ -7,11 +7,11 @@ class LoginEndpoint(BaseEndpoint): Login endpoint. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. username (str): username. password (str): password. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. @@ -44,11 +44,11 @@ def get_endpoint_options(host, port, username, password, version=DEFAULT_API_VER Logs in with given parameters and returns options to use for further calls to the RESTful API. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. username (str): username. password (str): password. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). Returns: diff --git a/exabyte_api_client/endpoints/logout.py b/src/py/mat3ra/api_client/endpoints/logout.py similarity index 88% rename from exabyte_api_client/endpoints/logout.py rename to src/py/mat3ra/api_client/endpoints/logout.py index 8cc615d..3d9d510 100644 --- a/exabyte_api_client/endpoints/logout.py +++ b/src/py/mat3ra/api_client/endpoints/logout.py @@ -7,11 +7,11 @@ class LogoutEndpoint(BaseEndpoint): Logout endpoint. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/materials.py b/src/py/mat3ra/api_client/endpoints/materials.py similarity index 95% rename from exabyte_api_client/endpoints/materials.py rename to src/py/mat3ra/api_client/endpoints/materials.py index dd22546..856e592 100644 --- a/exabyte_api_client/endpoints/materials.py +++ b/src/py/mat3ra/api_client/endpoints/materials.py @@ -12,11 +12,11 @@ class MaterialEndpoints(EntitySetEndpointsMixin, DefaultableEntityEndpointsMixin Material endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/metaproperties.py b/src/py/mat3ra/api_client/endpoints/metaproperties.py similarity index 89% rename from exabyte_api_client/endpoints/metaproperties.py rename to src/py/mat3ra/api_client/endpoints/metaproperties.py index 5dee0d7..7d131c2 100644 --- a/exabyte_api_client/endpoints/metaproperties.py +++ b/src/py/mat3ra/api_client/endpoints/metaproperties.py @@ -7,11 +7,11 @@ class MetaPropertiesEndpoints(BasePropertiesEndpoints): MetaProperties endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/mixins/__init__.py b/src/py/mat3ra/api_client/endpoints/mixins/__init__.py similarity index 100% rename from exabyte_api_client/endpoints/mixins/__init__.py rename to src/py/mat3ra/api_client/endpoints/mixins/__init__.py diff --git a/exabyte_api_client/endpoints/mixins/default.py b/src/py/mat3ra/api_client/endpoints/mixins/default.py similarity index 100% rename from exabyte_api_client/endpoints/mixins/default.py rename to src/py/mat3ra/api_client/endpoints/mixins/default.py diff --git a/exabyte_api_client/endpoints/mixins/set.py b/src/py/mat3ra/api_client/endpoints/mixins/set.py similarity index 100% rename from exabyte_api_client/endpoints/mixins/set.py rename to src/py/mat3ra/api_client/endpoints/mixins/set.py diff --git a/exabyte_api_client/endpoints/projects.py b/src/py/mat3ra/api_client/endpoints/projects.py similarity index 89% rename from exabyte_api_client/endpoints/projects.py rename to src/py/mat3ra/api_client/endpoints/projects.py index d11f94b..91adff4 100644 --- a/exabyte_api_client/endpoints/projects.py +++ b/src/py/mat3ra/api_client/endpoints/projects.py @@ -8,11 +8,11 @@ class ProjectEndpoints(DefaultableEntityEndpointsMixin, EntityEndpoint): Project endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/properties.py b/src/py/mat3ra/api_client/endpoints/properties.py similarity index 93% rename from exabyte_api_client/endpoints/properties.py rename to src/py/mat3ra/api_client/endpoints/properties.py index adab202..d6208cb 100644 --- a/exabyte_api_client/endpoints/properties.py +++ b/src/py/mat3ra/api_client/endpoints/properties.py @@ -26,11 +26,11 @@ class PropertiesEndpoints(BasePropertiesEndpoints): Properties endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/endpoints/workflows.py b/src/py/mat3ra/api_client/endpoints/workflows.py similarity index 88% rename from exabyte_api_client/endpoints/workflows.py rename to src/py/mat3ra/api_client/endpoints/workflows.py index 13f460a..5e256e3 100644 --- a/exabyte_api_client/endpoints/workflows.py +++ b/src/py/mat3ra/api_client/endpoints/workflows.py @@ -8,11 +8,11 @@ class WorkflowEndpoints(DefaultableEntityEndpointsMixin, EntityEndpoint): Workflow endpoints. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. + host (str): API hostname. + port (int): API port number. account_id (str): account ID. auth_token (str): authentication token. - version (str): Exabyte API version. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/utils/__init__.py b/src/py/mat3ra/api_client/utils/__init__.py similarity index 100% rename from exabyte_api_client/utils/__init__.py rename to src/py/mat3ra/api_client/utils/__init__.py diff --git a/exabyte_api_client/utils/http.py b/src/py/mat3ra/api_client/utils/http.py similarity index 96% rename from exabyte_api_client/utils/http.py rename to src/py/mat3ra/api_client/utils/http.py index bfc8096..73b80de 100644 --- a/exabyte_api_client/utils/http.py +++ b/src/py/mat3ra/api_client/utils/http.py @@ -117,9 +117,9 @@ class Connection(BaseConnection): Exabyte connection class. Args: - host (str): Exabyte API hostname. - port (int): Exabyte API port number. - version (str): Exabyte API version. + host (str): API hostname. + port (int): API port number. + version (str): API version. secure (bool): whether to use secure http protocol (https vs http). kwargs (dict): a dictionary of HTTP session options. timeout (int): session timeout in seconds. diff --git a/exabyte_api_client/utils/materials.py b/src/py/mat3ra/api_client/utils/materials.py similarity index 100% rename from exabyte_api_client/utils/materials.py rename to src/py/mat3ra/api_client/utils/materials.py diff --git a/tests/__init__.py b/tests/__init__.py index c756939..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,23 +0,0 @@ -import json -import os -import unittest - - -class EndpointBaseTest(unittest.TestCase): - """ - Base class for testing endpoints. - """ - - def __init__(self, *args, **kwargs): - super(EndpointBaseTest, self).__init__(*args, **kwargs) - - def get_file_path(self, filename): - return os.path.join(os.path.dirname(__file__), "data", filename) - - def get_content(self, filename): - with open(self.get_file_path(filename)) as f: - return f.read() - - def get_content_in_json(self, filename): - with open(self.get_file_path(filename)) as f: - return json.loads(f.read()) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index 9a99cfa..0000000 --- a/tests/integration/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -import os - -from tests import EndpointBaseTest - - -class BaseIntegrationTest(EndpointBaseTest): - """ - Base class for endpoints integration tests. - """ - - def __init__(self, *args, **kwargs): - super(BaseIntegrationTest, self).__init__(*args, **kwargs) - self.endpoint_kwargs = { - "host": os.environ["TEST_HOST"], - "port": os.environ["TEST_PORT"], - "account_id": os.environ["TEST_ACCOUNT_ID"], - "auth_token": os.environ["TEST_AUTH_TOKEN"], - "secure": os.environ.get("TEST_SECURE", "False").lower() == "true", - "version": os.environ.get("TEST_VERSION", "2018-10-01"), - } diff --git a/tests/integration/entity.py b/tests/integration/entity.py deleted file mode 100644 index 5d8381a..0000000 --- a/tests/integration/entity.py +++ /dev/null @@ -1,80 +0,0 @@ -import time - -from requests.exceptions import HTTPError - -from tests.integration import BaseIntegrationTest - - -class EntityIntegrationTest(BaseIntegrationTest): - """ - Base entity integration tests class. - """ - - def __init__(self, *args, **kwargs): - super(EntityIntegrationTest, self).__init__(*args, **kwargs) - self.endpoints = None - self.entity_id: str = "" - - def entities_selector(self): - """ - Returns a selector to access entities created in tests. - Override upon inheritance as necessary. - """ - return {"tags": "INTEGRATION-TEST"} - - def tearDown(self): - """Delete only the current test entity if it still exists after test. - - Warn if the filtering fails, failsafe attempt to delete the entity anyways. - """ - tagged_test_entity_id_list = [e["_id"] for e in self.endpoints.list(query=self.entities_selector())] - try: - if self.entity_id not in tagged_test_entity_id_list: - print( - f"WARNING: Entity with ID {self.entity_id} not found in the list of tagged entities:" - f" {tagged_test_entity_id_list}" - ) - self.endpoints.delete(self.entity_id) - - except HTTPError as e: - print(f"WARNING: Failed to delete entity with ID {self.entity_id}: {e}") - - def get_default_config(self): - """ - Returns the default entity config. - Override upon inheritance. - """ - raise NotImplementedError - - def create_entity(self, kwargs=None): - entity = self.get_default_config() - entity.update(kwargs or {}) - entity.setdefault("tags", []).append("INTEGRATION-TEST") - created_entity = self.endpoints.create(entity) - self.entity_id = created_entity["_id"] - return created_entity - - def list_entities_test(self): - entity = self.create_entity() - self.assertIn(entity["_id"], [e["_id"] for e in self.endpoints.list(query=self.entities_selector())]) - - def get_entity_by_id_test(self): - entity = self.create_entity() - self.assertEqual(self.endpoints.get(entity["_id"])["_id"], entity["_id"]) - - def create_entity_test(self): - name = "test-{}".format(time.time()) - entity = self.create_entity({"name": name}) - self.assertEqual(entity["name"], name) - self.assertIsNotNone(entity["_id"]) - self.assertIn("INTEGRATION-TEST", entity["tags"]) - - def delete_entity_test(self): - entity = self.create_entity() - self.endpoints.delete(entity["_id"]) - self.assertNotIn(entity["_id"], [e["_id"] for e in self.endpoints.list(query=self.entities_selector())]) - - def update_entity_test(self): - entity = self.create_entity() - updated_entity = self.endpoints.update(entity["_id"], {"name": "UPDATED"}) - self.assertEqual(updated_entity["name"], "UPDATED") diff --git a/tests/py/__init__.py b/tests/py/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/py/conftest.py b/tests/py/conftest.py new file mode 100644 index 0000000..d6a7641 --- /dev/null +++ b/tests/py/conftest.py @@ -0,0 +1,27 @@ +""" +Shared test fixtures and base classes for pytest. +""" +import json +import os +import unittest + + +class EndpointBaseTest(unittest.TestCase): + """ + Base class for testing endpoints. + """ + + def __init__(self, *args, **kwargs): + super(EndpointBaseTest, self).__init__(*args, **kwargs) + + def get_file_path(self, filename): + return os.path.join(os.path.dirname(__file__), "data", filename) + + def get_content(self, filename): + with open(self.get_file_path(filename)) as f: + return f.read() + + def get_content_in_json(self, filename): + with open(self.get_file_path(filename)) as f: + return json.loads(f.read()) + diff --git a/tests/data/login.json b/tests/py/data/login.json similarity index 100% rename from tests/data/login.json rename to tests/py/data/login.json diff --git a/tests/data/logout.json b/tests/py/data/logout.json similarity index 100% rename from tests/data/logout.json rename to tests/py/data/logout.json diff --git a/tests/data/material.json b/tests/py/data/material.json similarity index 100% rename from tests/data/material.json rename to tests/py/data/material.json diff --git a/tests/py/integration/__init__.py b/tests/py/integration/__init__.py new file mode 100644 index 0000000..d7ddfc9 --- /dev/null +++ b/tests/py/integration/__init__.py @@ -0,0 +1,35 @@ +import os + +import pytest +from tests.py.conftest import EndpointBaseTest + +DEFAULT_TEST_SECURE = "False" +DEFAULT_TEST_VERSION = "2018-10-01" + +REQUIRED_ENV_VARS = ["TEST_HOST", "TEST_PORT", "TEST_ACCOUNT_ID", "TEST_AUTH_TOKEN"] + + +def _check_integration_env(): + """Check if required integration test environment variables are set.""" + missing = [var for var in REQUIRED_ENV_VARS if var not in os.environ] + if missing: + pytest.skip("Integration tests require environment variables: TEST_HOST, TEST_PORT, TEST_ACCOUNT_ID, " + + f"TEST_AUTH_TOKEN. Set them to run integration tests. Missing: {', '.join(missing)}") + + +class BaseIntegrationTest(EndpointBaseTest): + """ + Base class for endpoints integration tests. + """ + + def __init__(self, *args, **kwargs): + super(BaseIntegrationTest, self).__init__(*args, **kwargs) + _check_integration_env() + self.endpoint_kwargs = { + "host": os.environ["TEST_HOST"], + "port": os.environ["TEST_PORT"], + "account_id": os.environ["TEST_ACCOUNT_ID"], + "auth_token": os.environ["TEST_AUTH_TOKEN"], + "secure": os.environ.get("TEST_SECURE", DEFAULT_TEST_SECURE).lower() == "true", + "version": os.environ.get("TEST_VERSION", DEFAULT_TEST_VERSION), + } diff --git a/tests/py/integration/entity.py b/tests/py/integration/entity.py new file mode 100644 index 0000000..cd3ff73 --- /dev/null +++ b/tests/py/integration/entity.py @@ -0,0 +1,88 @@ +import time + +from requests.exceptions import HTTPError + +from tests.py.integration import BaseIntegrationTest + +INTEGRATION_TEST_TAG = "INTEGRATION-TEST" +ENTITY_ID_KEY = "_id" +ENTITY_NAME_KEY = "name" +ENTITY_TAGS_KEY = "tags" +TEST_NAME_PREFIX = "test-" +UPDATED_NAME = "UPDATED" +WARNING_ENTITY_NOT_FOUND = "WARNING: Entity with ID {} not found in the list of tagged entities: {}" +WARNING_DELETE_FAILED = "WARNING: Failed to delete entity with ID {}: {}" + + +class EntityIntegrationTest(BaseIntegrationTest): + """ + Base entity integration tests class. + """ + + def __init__(self, *args, **kwargs): + super(EntityIntegrationTest, self).__init__(*args, **kwargs) + self.endpoints = None + self.entity_id: str = "" + + def entities_selector(self): + """ + Returns a selector to access entities created in tests. + Override upon inheritance as necessary. + """ + return {ENTITY_TAGS_KEY: INTEGRATION_TEST_TAG} + + def tearDown(self): + """Delete only the current test entity if it still exists after test. + + Warn if the filtering fails, failsafe attempt to delete the entity anyways. + """ + tagged_test_entity_id_list = [e[ENTITY_ID_KEY] for e in self.endpoints.list(query=self.entities_selector())] + try: + if self.entity_id not in tagged_test_entity_id_list: + print(WARNING_ENTITY_NOT_FOUND.format(self.entity_id, tagged_test_entity_id_list)) + self.endpoints.delete(self.entity_id) + + except HTTPError as e: + print(WARNING_DELETE_FAILED.format(self.entity_id, e)) + + def get_default_config(self): + """ + Returns the default entity config. + Override upon inheritance. + """ + raise NotImplementedError + + def create_entity(self, kwargs=None): + entity = self.get_default_config() + entity.update(kwargs or {}) + entity.setdefault(ENTITY_TAGS_KEY, []).append(INTEGRATION_TEST_TAG) + created_entity = self.endpoints.create(entity) + self.entity_id = created_entity[ENTITY_ID_KEY] + return created_entity + + def list_entities_test(self): + entity = self.create_entity() + self.assertIn(entity[ENTITY_ID_KEY], + [e[ENTITY_ID_KEY] for e in self.endpoints.list(query=self.entities_selector())]) + + def get_entity_by_id_test(self): + entity = self.create_entity() + self.assertEqual(self.endpoints.get(entity[ENTITY_ID_KEY])[ENTITY_ID_KEY], entity[ENTITY_ID_KEY]) + + def create_entity_test(self): + name = f"{TEST_NAME_PREFIX}{time.time()}" + entity = self.create_entity({ENTITY_NAME_KEY: name}) + self.assertEqual(entity[ENTITY_NAME_KEY], name) + self.assertIsNotNone(entity[ENTITY_ID_KEY]) + self.assertIn(INTEGRATION_TEST_TAG, entity[ENTITY_TAGS_KEY]) + + def delete_entity_test(self): + entity = self.create_entity() + self.endpoints.delete(entity[ENTITY_ID_KEY]) + self.assertNotIn(entity[ENTITY_ID_KEY], + [e[ENTITY_ID_KEY] for e in self.endpoints.list(query=self.entities_selector())]) + + def update_entity_test(self): + entity = self.create_entity() + updated_entity = self.endpoints.update(entity[ENTITY_ID_KEY], {ENTITY_NAME_KEY: UPDATED_NAME}) + self.assertEqual(updated_entity[ENTITY_NAME_KEY], UPDATED_NAME) diff --git a/tests/integration/test_jobs.py b/tests/py/integration/test_jobs.py similarity index 57% rename from tests/integration/test_jobs.py rename to tests/py/integration/test_jobs.py index e3136a6..6d85d1f 100644 --- a/tests/integration/test_jobs.py +++ b/tests/py/integration/test_jobs.py @@ -1,8 +1,24 @@ import datetime import time -from exabyte_api_client.endpoints.jobs import JobEndpoints -from tests.integration.entity import EntityIntegrationTest +from mat3ra.api_client.endpoints.jobs import JobEndpoints +from tests.py.integration.entity import EntityIntegrationTest + +KNOWN_COMPLETED_JOB_ID = "9gyhfncWDhnSyzALv" +JOB_NAME_PREFIX = "API-CLIENT TEST JOB" +DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" +DEFAULT_NODES = 1 +DEFAULT_NOTIFY = "n" +DEFAULT_PPN = 1 +DEFAULT_QUEUE = "D" +DEFAULT_TIME_LIMIT = "00:05:00" +TEST_TIME_LIMIT = "00:10:00" +TEST_NOTIFY = "abe" +JOB_STATUS_SUBMITTED = "submitted" +JOB_STATUS_FINISHED = "finished" +JOB_WAIT_TIMEOUT = 900 +JOB_WAIT_SLEEP_INTERVAL = 5 +JOB_TIMEOUT_ERROR = "job with ID {} did not finish within {} seconds" class JobEndpointsIntegrationTest(EntityIntegrationTest): @@ -10,8 +26,6 @@ class JobEndpointsIntegrationTest(EntityIntegrationTest): Job endpoints integration tests. """ - KNOWN_COMPLETED_JOB_ID = "9gyhfncWDhnSyzALv" - def __init__(self, *args, **kwargs): super(JobEndpointsIntegrationTest, self).__init__(*args, **kwargs) self.endpoints = JobEndpoints(**self.endpoint_kwargs) @@ -21,10 +35,11 @@ def get_default_config(self): Returns the default entity config. Override upon inheritance. """ - now_time = datetime.datetime.today().strftime("%Y-%m-%d %H:%M:%S") - return {"name": "API-CLIENT TEST JOB {}".format(now_time)} + now_time = datetime.datetime.today().strftime(DATETIME_FORMAT) + return {"name": f"{JOB_NAME_PREFIX} {now_time}"} - def get_compute_params(self, nodes=1, notify="n", ppn=1, queue="D", time_limit="00:05:00"): + def get_compute_params(self, nodes=DEFAULT_NODES, notify=DEFAULT_NOTIFY, ppn=DEFAULT_PPN, queue=DEFAULT_QUEUE, + time_limit=DEFAULT_TIME_LIMIT): return {"nodes": nodes, "notify": notify, "ppn": ppn, "queue": queue, "timeLimit": time_limit} def test_list_jobs(self): @@ -42,34 +57,33 @@ def test_delete_job(self): def test_update_job(self): self.update_entity_test() - def _wait_for_job_to_finish(self, id_, timeout=900): + def _wait_for_job_to_finish(self, id_, timeout=JOB_WAIT_TIMEOUT): end = time.time() + timeout while time.time() < end: job = self.endpoints.get(id_) - if job["status"] == "finished": + if job["status"] == JOB_STATUS_FINISHED: return - time.sleep(5) - raise BaseException("job with ID {} did not finish within {} seconds".format(id_, timeout)) + time.sleep(JOB_WAIT_SLEEP_INTERVAL) + raise BaseException(JOB_TIMEOUT_ERROR.format(id_, timeout)) def test_submit_job_and_wait_to_finish(self): job = self.create_entity() self.endpoints.submit(job["_id"]) - self.assertEqual("submitted", self.endpoints.get(job["_id"])["status"]) + self.assertEqual(JOB_STATUS_SUBMITTED, self.endpoints.get(job["_id"])["status"]) self._wait_for_job_to_finish(job["_id"]) def test_create_job_timeLimit(self): - time_limit = "00:10:00" - job = self.create_entity({"compute": self.get_compute_params(time_limit=time_limit)}) + job = self.create_entity({"compute": self.get_compute_params(time_limit=TEST_TIME_LIMIT)}) self.assertEqual(self.endpoints.get(job["_id"])["_id"], job["_id"]) - self.assertEqual(self.endpoints.get(job["_id"])["compute"]["timeLimit"], time_limit) + self.assertEqual(self.endpoints.get(job["_id"])["compute"]["timeLimit"], TEST_TIME_LIMIT) def test_create_job_notify(self): - job = self.create_entity({"compute": self.get_compute_params(notify="abe")}) + job = self.create_entity({"compute": self.get_compute_params(notify=TEST_NOTIFY)}) self.assertEqual(self.endpoints.get(job["_id"])["_id"], job["_id"]) - self.assertEqual(self.endpoints.get(job["_id"])["compute"]["notify"], "abe") + self.assertEqual(self.endpoints.get(job["_id"])["compute"]["notify"], TEST_NOTIFY) def test_list_files(self): - http_response_data = self.endpoints.list_files(self.KNOWN_COMPLETED_JOB_ID) + http_response_data = self.endpoints.list_files(KNOWN_COMPLETED_JOB_ID) self.assertIsInstance(http_response_data, list) self.assertGreater(len(http_response_data), 0) self.assertIsInstance(http_response_data[0], dict) diff --git a/tests/integration/test_materials.py b/tests/py/integration/test_materials.py similarity index 76% rename from tests/integration/test_materials.py rename to tests/py/integration/test_materials.py index 2ba28aa..daecba5 100644 --- a/tests/integration/test_materials.py +++ b/tests/py/integration/test_materials.py @@ -1,5 +1,8 @@ -from exabyte_api_client.endpoints.materials import MaterialEndpoints -from tests.integration.entity import EntityIntegrationTest +from mat3ra.api_client.endpoints.materials import MaterialEndpoints +from tests.py.integration.entity import EntityIntegrationTest + +MATERIAL_DATA_FILE = "material.json" +TEST_FORMULA = "Si" class MaterialEndpointsIntegrationTest(EntityIntegrationTest): @@ -12,7 +15,7 @@ def __init__(self, *args, **kwargs): self.endpoints = MaterialEndpoints(**self.endpoint_kwargs) def get_default_config(self): - return self.get_content_in_json("material.json") + return self.get_content_in_json(MATERIAL_DATA_FILE) def test_list_materials(self): self.list_entities_test() @@ -31,5 +34,5 @@ def test_update_material(self): def test_get_material_by_formula(self): material = self.create_entity() - materials = self.endpoints.list(query={"_id": material["_id"], "formula": "Si"}) + materials = self.endpoints.list(query={"_id": material["_id"], "formula": TEST_FORMULA}) self.assertIn(material["_id"], [m["_id"] for m in materials]) diff --git a/tests/unit/__init__.py b/tests/py/unit/__init__.py similarity index 93% rename from tests/unit/__init__.py rename to tests/py/unit/__init__.py index ae8737b..3cf693a 100644 --- a/tests/unit/__init__.py +++ b/tests/py/unit/__init__.py @@ -1,6 +1,6 @@ from requests import Response -from tests import EndpointBaseTest +from tests.py.conftest import EndpointBaseTest class EndpointBaseUnitTest(EndpointBaseTest): diff --git a/tests/py/unit/entity.py b/tests/py/unit/entity.py new file mode 100644 index 0000000..56f8390 --- /dev/null +++ b/tests/py/unit/entity.py @@ -0,0 +1,44 @@ +from tests.py.unit import EndpointBaseUnitTest + +TEST_ENTITY_ID = "28FMvD5knJZZx452H" +MOCK_SUCCESS_RESPONSE_LIST = '{"status": "success", "data": []}' +MOCK_SUCCESS_RESPONSE_OBJECT = '{"status": "success", "data": {}}' +HTTP_METHOD_GET = "get" +HTTP_METHOD_DELETE = "delete" +CONTENT_TYPE_JSON = "application/json" + + +class EntityEndpointsUnitTest(EndpointBaseUnitTest): + """ + Base class for testing entity endpoints. + """ + + def __init__(self, *args, **kwargs): + super(EntityEndpointsUnitTest, self).__init__(*args, **kwargs) + self.endpoints = None + self.endpoint_name = None + + @property + def base_url(self): + return f"https://{self.host}:{self.port}/api/{self.version}/{self.endpoint_name}" + + def list(self, mock_request): + mock_request.return_value = self.mock_response(MOCK_SUCCESS_RESPONSE_LIST) + self.assertEqual(self.endpoints.list(), []) + self.assertEqual(mock_request.call_args[1]["method"], HTTP_METHOD_GET) + + def get(self, mock_request): + mock_request.return_value = self.mock_response(MOCK_SUCCESS_RESPONSE_OBJECT) + self.assertEqual(self.endpoints.get(TEST_ENTITY_ID), {}) + expected_url = f"{self.base_url}/{TEST_ENTITY_ID}" + self.assertEqual(mock_request.call_args[1]["url"], expected_url) + + def create(self, mock_request): + mock_request.return_value = self.mock_response(MOCK_SUCCESS_RESPONSE_OBJECT) + self.endpoints.create({}) + self.assertEqual(mock_request.call_args[1]["headers"]["Content-Type"], CONTENT_TYPE_JSON) + + def delete(self, mock_request): + mock_request.return_value = self.mock_response(MOCK_SUCCESS_RESPONSE_OBJECT) + self.assertEqual(self.endpoints.delete(TEST_ENTITY_ID), {}) + self.assertEqual(mock_request.call_args[1]["method"], HTTP_METHOD_DELETE) diff --git a/tests/unit/test_bank_materials.py b/tests/py/unit/test_bank_materials.py similarity index 74% rename from tests/unit/test_bank_materials.py rename to tests/py/unit/test_bank_materials.py index 4372ede..6d3591d 100644 --- a/tests/unit/test_bank_materials.py +++ b/tests/py/unit/test_bank_materials.py @@ -1,6 +1,9 @@ from unittest import mock -from exabyte_api_client.endpoints.bank_materials import BankMaterialEndpoints -from tests.unit.entity import EntityEndpointsUnitTest + +from mat3ra.api_client.endpoints.bank_materials import BankMaterialEndpoints +from tests.py.unit.entity import EntityEndpointsUnitTest + +ENDPOINT_NAME = "bank-materials" class EndpointMaterialsBankUnitTest(EntityEndpointsUnitTest): @@ -10,7 +13,7 @@ class EndpointMaterialsBankUnitTest(EntityEndpointsUnitTest): def __init__(self, *args, **kwargs): super(EndpointMaterialsBankUnitTest, self).__init__(*args, **kwargs) - self.endpoint_name = "bank-materials" + self.endpoint_name = ENDPOINT_NAME self.endpoints = BankMaterialEndpoints(self.host, self.port, self.account_id, self.auth_token) @mock.patch("requests.sessions.Session.request") diff --git a/tests/unit/test_bank_workflows.py b/tests/py/unit/test_bank_workflows.py similarity index 74% rename from tests/unit/test_bank_workflows.py rename to tests/py/unit/test_bank_workflows.py index 0c2cab6..2081311 100644 --- a/tests/unit/test_bank_workflows.py +++ b/tests/py/unit/test_bank_workflows.py @@ -1,6 +1,9 @@ from unittest import mock -from exabyte_api_client.endpoints.bank_workflows import BankWorkflowEndpoints -from tests.unit.entity import EntityEndpointsUnitTest + +from mat3ra.api_client.endpoints.bank_workflows import BankWorkflowEndpoints +from tests.py.unit.entity import EntityEndpointsUnitTest + +ENDPOINT_NAME = "bank-workflows" class EndpointWorkflowsBankUnitTest(EntityEndpointsUnitTest): @@ -10,7 +13,7 @@ class EndpointWorkflowsBankUnitTest(EntityEndpointsUnitTest): def __init__(self, *args, **kwargs): super(EndpointWorkflowsBankUnitTest, self).__init__(*args, **kwargs) - self.endpoint_name = "bank-workflows" + self.endpoint_name = ENDPOINT_NAME self.endpoints = BankWorkflowEndpoints(self.host, self.port, self.account_id, self.auth_token) @mock.patch("requests.sessions.Session.request") diff --git a/tests/unit/test_client.py b/tests/py/unit/test_client.py similarity index 79% rename from tests/unit/test_client.py rename to tests/py/unit/test_client.py index 911624c..59306ad 100644 --- a/tests/unit/test_client.py +++ b/tests/py/unit/test_client.py @@ -1,10 +1,9 @@ import os from unittest import mock -from pydantic import ValidationError +from mat3ra.api_client import APIClient -from exabyte_api_client import APIClient -from tests.unit import EndpointBaseUnitTest +from tests.py.unit import EndpointBaseUnitTest API_HOST = "platform.mat3ra.com" API_PORT = "4000" @@ -34,22 +33,6 @@ def _mock_users_me(self, mock_get): mock_resp.raise_for_status.return_value = None mock_get.return_value = mock_resp - def test_authenticate_requires_env_config(self): - cases = [ - ("API_HOST",), - ("API_PORT",), - ("API_VERSION",), - ("API_SECURE",), - ] - for (missing_key,) in cases: - with self.subTest(missing_key=missing_key): - env = self._base_env() - env.pop(missing_key) - env["OIDC_ACCESS_TOKEN"] = OIDC_ACCESS_TOKEN - with mock.patch.dict("os.environ", env, clear=True): - with self.assertRaises(ValidationError): - APIClient.authenticate() - def test_authenticate_requires_auth(self): env = self._base_env() with mock.patch.dict("os.environ", env, clear=True): @@ -88,5 +71,3 @@ def test_my_account_id_fetches_and_caches(self, mock_get): self.assertEqual(mock_get.call_args[1]["headers"]["Authorization"], f"Bearer {OIDC_ACCESS_TOKEN}") self.assertEqual(mock_get.call_args[1]["timeout"], 30) self.assertEqual(os.environ.get("ACCOUNT_ID"), ME_ACCOUNT_ID) - - diff --git a/tests/py/unit/test_httpBase.py b/tests/py/unit/test_httpBase.py new file mode 100644 index 0000000..be3a8dc --- /dev/null +++ b/tests/py/unit/test_httpBase.py @@ -0,0 +1,43 @@ +from unittest import mock + +from mat3ra.api_client.utils.http import Connection +from requests.exceptions import HTTPError +from tests.py.unit import EndpointBaseUnitTest + +API_VERSION_1 = "2018-10-1" +API_VERSION_2 = "2018-10-2" +HTTP_STATUS_UNAUTHORIZED = 401 +HTTP_REASON_UNAUTHORIZED = "Unauthorized" +EMPTY_CONTENT = "" +TEST_ENTITY_ID = "28FMvD5knJZZx452H" +EMPTY_USERNAME = "" +EMPTY_PASSWORD = "" + + +class HTTPBaseUnitTest(EndpointBaseUnitTest): + """ + Class for testing functionality implemented inside HTTPBase module. + """ + + def __init__(self, *args, **kwargs): + super(HTTPBaseUnitTest, self).__init__(*args, **kwargs) + + def test_preamble_secure(self): + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + self.assertEqual(conn.preamble, f"https://{self.host}:{self.port}/api/{API_VERSION_1}/") + + def test_preamble_unsecure(self): + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=False) + self.assertEqual(conn.preamble, f"http://{self.host}:{self.port}/api/{API_VERSION_1}/") + + def test_preamble_version(self): + conn = Connection(self.host, self.port, version=API_VERSION_2, secure=True) + self.assertEqual(conn.preamble, f"https://{self.host}:{self.port}/api/{API_VERSION_2}/") + + @mock.patch("requests.sessions.Session.request") + def test_raise_http_error(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_UNAUTHORIZED, + reason=HTTP_REASON_UNAUTHORIZED) + with self.assertRaises(HTTPError): + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("POST", "login", data={"username": EMPTY_USERNAME, "password": EMPTY_PASSWORD}) diff --git a/tests/unit/test_jobs.py b/tests/py/unit/test_jobs.py similarity index 82% rename from tests/unit/test_jobs.py rename to tests/py/unit/test_jobs.py index a656861..1e69c40 100644 --- a/tests/unit/test_jobs.py +++ b/tests/py/unit/test_jobs.py @@ -1,7 +1,9 @@ from unittest import mock -from exabyte_api_client.endpoints.jobs import JobEndpoints -from tests.unit.entity import EntityEndpointsUnitTest +from mat3ra.api_client.endpoints.jobs import JobEndpoints +from tests.py.unit.entity import EntityEndpointsUnitTest + +ENDPOINT_NAME = "jobs" class EndpointJobsUnitTest(EntityEndpointsUnitTest): @@ -11,7 +13,7 @@ class EndpointJobsUnitTest(EntityEndpointsUnitTest): def __init__(self, *args, **kwargs): super(EndpointJobsUnitTest, self).__init__(*args, **kwargs) - self.endpoint_name = "jobs" + self.endpoint_name = ENDPOINT_NAME self.endpoints = JobEndpoints(self.host, self.port, self.account_id, self.auth_token) @mock.patch("requests.sessions.Session.request") diff --git a/tests/unit/test_login.py b/tests/py/unit/test_login.py similarity index 56% rename from tests/unit/test_login.py rename to tests/py/unit/test_login.py index a27444f..5ec6a29 100644 --- a/tests/unit/test_login.py +++ b/tests/py/unit/test_login.py @@ -1,7 +1,16 @@ from unittest import mock -from exabyte_api_client.endpoints.login import LoginEndpoint -from tests.unit import EndpointBaseUnitTest +from mat3ra.api_client.endpoints.login import LoginEndpoint +from tests.py.unit import EndpointBaseUnitTest + +TEST_USERNAME = "test" +TEST_PASSWORD = "test" +LOGIN_RESPONSE_FILE = "login.json" + +EXPECTED_LOGIN_RESULT = { + "X-Account-Id": "ubxMkAyx37Rjn8qK9", + "X-Auth-Token": "XihOnUA8EqytSui1icz6fYhsJ2tUsJGGTlV03upYPSF", +} class EndpointLoginUnitTest(EndpointBaseUnitTest): @@ -11,17 +20,13 @@ class EndpointLoginUnitTest(EndpointBaseUnitTest): def __init__(self, *args, **kwargs): super(EndpointLoginUnitTest, self).__init__(*args, **kwargs) - self.username = "test" - self.password = "test" + self.username = TEST_USERNAME + self.password = TEST_PASSWORD self.login_endpoint = LoginEndpoint(self.host, self.port, self.username, self.password) @mock.patch("requests.sessions.Session.request") def test_login(self, mock_request): - mock_request.return_value = self.mock_response(self.get_content("login.json")) - expected_result = { - "X-Account-Id": "ubxMkAyx37Rjn8qK9", - "X-Auth-Token": "XihOnUA8EqytSui1icz6fYhsJ2tUsJGGTlV03upYPSF", - } - self.assertEqual(self.login_endpoint.login(), expected_result) + mock_request.return_value = self.mock_response(self.get_content(LOGIN_RESPONSE_FILE)) + self.assertEqual(self.login_endpoint.login(), EXPECTED_LOGIN_RESULT) self.assertEqual(mock_request.call_args[1]["data"]["username"], self.username) self.assertEqual(mock_request.call_args[1]["data"]["password"], self.password) diff --git a/tests/unit/test_logout.py b/tests/py/unit/test_logout.py similarity index 67% rename from tests/unit/test_logout.py rename to tests/py/unit/test_logout.py index 7ee1e82..6bab97c 100644 --- a/tests/unit/test_logout.py +++ b/tests/py/unit/test_logout.py @@ -1,7 +1,13 @@ from unittest import mock -from exabyte_api_client.endpoints.logout import LogoutEndpoint -from tests.unit import EndpointBaseUnitTest +from mat3ra.api_client.endpoints.logout import LogoutEndpoint +from tests.py.unit import EndpointBaseUnitTest + +LOGOUT_RESPONSE_FILE = "logout.json" + +EXPECTED_LOGOUT_RESULT = { + "message": "You are successfully logged out" +} class EndpointLogoutUnitTest(EndpointBaseUnitTest): @@ -15,7 +21,7 @@ def __init__(self, *args, **kwargs): @mock.patch("requests.sessions.Session.request") def test_logout(self, mock_request): - mock_request.return_value = self.mock_response(self.get_content("logout.json")) - self.assertEqual(self.logout_endpoint.logout(), {"message": "You are successfully logged out"}) + mock_request.return_value = self.mock_response(self.get_content(LOGOUT_RESPONSE_FILE)) + self.assertEqual(self.logout_endpoint.logout(), EXPECTED_LOGOUT_RESULT) self.assertEqual(mock_request.call_args[1]["headers"]["X-Account-Id"], self.account_id) self.assertEqual(mock_request.call_args[1]["headers"]["X-Auth-Token"], self.auth_token) diff --git a/tests/unit/test_materials.py b/tests/py/unit/test_materials.py similarity index 81% rename from tests/unit/test_materials.py rename to tests/py/unit/test_materials.py index 073c583..4dcb849 100644 --- a/tests/unit/test_materials.py +++ b/tests/py/unit/test_materials.py @@ -1,7 +1,9 @@ from unittest import mock -from exabyte_api_client.endpoints.materials import MaterialEndpoints -from tests.unit.entity import EntityEndpointsUnitTest +from mat3ra.api_client.endpoints.materials import MaterialEndpoints +from tests.py.unit.entity import EntityEndpointsUnitTest + +ENDPOINT_NAME = "materials" class EndpointMaterialsUnitTest(EntityEndpointsUnitTest): @@ -11,7 +13,7 @@ class EndpointMaterialsUnitTest(EntityEndpointsUnitTest): def __init__(self, *args, **kwargs): super(EndpointMaterialsUnitTest, self).__init__(*args, **kwargs) - self.endpoint_name = "materials" + self.endpoint_name = ENDPOINT_NAME self.endpoints = MaterialEndpoints(self.host, self.port, self.account_id, self.auth_token) @mock.patch("requests.sessions.Session.request") diff --git a/tests/unit/test_properties.py b/tests/py/unit/test_properties.py similarity index 75% rename from tests/unit/test_properties.py rename to tests/py/unit/test_properties.py index f9c5302..9cad06c 100644 --- a/tests/unit/test_properties.py +++ b/tests/py/unit/test_properties.py @@ -1,6 +1,9 @@ from unittest import mock -from exabyte_api_client.endpoints.properties import PropertiesEndpoints -from tests.unit.entity import EntityEndpointsUnitTest + +from mat3ra.api_client.endpoints.properties import PropertiesEndpoints +from tests.py.unit.entity import EntityEndpointsUnitTest + +ENDPOINT_NAME = "properties" class EndpointCharacteristicUnitTest(EntityEndpointsUnitTest): @@ -10,7 +13,7 @@ class EndpointCharacteristicUnitTest(EntityEndpointsUnitTest): def __init__(self, *args, **kwargs): super(EndpointCharacteristicUnitTest, self).__init__(*args, **kwargs) - self.endpoint_name = "properties" + self.endpoint_name = ENDPOINT_NAME self.endpoints = PropertiesEndpoints(self.host, self.port, self.account_id, self.auth_token) @mock.patch("requests.sessions.Session.request") diff --git a/tests/unit/test_workflows.py b/tests/py/unit/test_workflows.py similarity index 81% rename from tests/unit/test_workflows.py rename to tests/py/unit/test_workflows.py index 43cade9..9586a85 100644 --- a/tests/unit/test_workflows.py +++ b/tests/py/unit/test_workflows.py @@ -1,7 +1,9 @@ from unittest import mock -from exabyte_api_client.endpoints.workflows import WorkflowEndpoints -from tests.unit.entity import EntityEndpointsUnitTest +from mat3ra.api_client.endpoints.workflows import WorkflowEndpoints +from tests.py.unit.entity import EntityEndpointsUnitTest + +ENDPOINT_NAME = "workflows" class EndpointWorkflowsUnitTest(EntityEndpointsUnitTest): @@ -11,7 +13,7 @@ class EndpointWorkflowsUnitTest(EntityEndpointsUnitTest): def __init__(self, *args, **kwargs): super(EndpointWorkflowsUnitTest, self).__init__(*args, **kwargs) - self.endpoint_name = "workflows" + self.endpoint_name = ENDPOINT_NAME self.endpoints = WorkflowEndpoints(self.host, self.port, self.account_id, self.auth_token) @mock.patch("requests.sessions.Session.request") diff --git a/tests/unit/entity.py b/tests/unit/entity.py deleted file mode 100644 index 69faeda..0000000 --- a/tests/unit/entity.py +++ /dev/null @@ -1,37 +0,0 @@ -from tests.unit import EndpointBaseUnitTest - - -class EntityEndpointsUnitTest(EndpointBaseUnitTest): - """ - Base class for testing entity endpoints. - """ - - def __init__(self, *args, **kwargs): - super(EntityEndpointsUnitTest, self).__init__(*args, **kwargs) - self.endpoints = None - self.endpoint_name = None - - @property - def base_url(self): - return "https://{}:{}/api/{}/{}".format(self.host, self.port, self.version, self.endpoint_name) - - def list(self, mock_request): - mock_request.return_value = self.mock_response('{"status": "success", "data": []}') - self.assertEqual(self.endpoints.list(), []) - self.assertEqual(mock_request.call_args[1]["method"], "get") - - def get(self, mock_request): - mock_request.return_value = self.mock_response('{"status": "success", "data": {}}') - self.assertEqual(self.endpoints.get("28FMvD5knJZZx452H"), {}) - expected_url = "{}/28FMvD5knJZZx452H".format(self.base_url) - self.assertEqual(mock_request.call_args[1]["url"], expected_url) - - def create(self, mock_request): - mock_request.return_value = self.mock_response('{"status": "success", "data": {}}') - self.endpoints.create({}) - self.assertEqual(mock_request.call_args[1]["headers"]["Content-Type"], "application/json") - - def delete(self, mock_request): - mock_request.return_value = self.mock_response('{"status": "success", "data": {}}') - self.assertEqual(self.endpoints.delete("28FMvD5knJZZx452H"), {}) - self.assertEqual(mock_request.call_args[1]["method"], "delete") diff --git a/tests/unit/test_httpBase.py b/tests/unit/test_httpBase.py deleted file mode 100644 index 4a9cbcb..0000000 --- a/tests/unit/test_httpBase.py +++ /dev/null @@ -1,32 +0,0 @@ -from unittest import mock -from exabyte_api_client.utils.http import Connection -from requests.exceptions import HTTPError -from tests.unit import EndpointBaseUnitTest - - -class HTTPBaseUnitTest(EndpointBaseUnitTest): - """ - Class for testing functionality implemented inside HTTPBase module. - """ - - def __init__(self, *args, **kwargs): - super(HTTPBaseUnitTest, self).__init__(*args, **kwargs) - - def test_preamble_secure(self): - conn = Connection(self.host, self.port, version="2018-10-1", secure=True) - self.assertEqual(conn.preamble, "https://{}:{}/api/2018-10-1/".format(self.host, self.port)) - - def test_preamble_unsecure(self): - conn = Connection(self.host, self.port, version="2018-10-1", secure=False) - self.assertEqual(conn.preamble, "http://{}:{}/api/2018-10-1/".format(self.host, self.port)) - - def test_preamble_version(self): - conn = Connection(self.host, self.port, version="2018-10-2", secure=True) - self.assertEqual(conn.preamble, "https://{}:{}/api/2018-10-2/".format(self.host, self.port)) - - @mock.patch("requests.sessions.Session.request") - def test_raise_http_error(self, mock_request): - mock_request.return_value = self.mock_response("", 401, reason="Unauthorized") - with self.assertRaises(HTTPError): - conn = Connection(self.host, self.port, version="2018-10-1", secure=True) - conn.request("POST", "login", data={"username": "", "password": ""})