From 9873c28382a6b7db9d1ebb1adc715b7f3663fe18 Mon Sep 17 00:00:00 2001 From: David Freidin Date: Wed, 18 Mar 2026 10:31:02 -0700 Subject: [PATCH 1/2] ENG-9909 add support for path-based multi-tenancy --- README.md | 22 ++++- smsdk/Auth/auth.py | 1 + smsdk/client.py | 30 +++++- smsdk/client_v0.py | 49 +++++++--- smsdk/utils.py | 17 +++- tests/Uri/test_uri.py | 158 ++++++++++++++++++++++++++++++++ tests/test_url_utils.py | 196 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 455 insertions(+), 18 deletions(-) create mode 100644 tests/test_url_utils.py diff --git a/README.md b/README.md index 5be9e7f..ed95e40 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ When accessing Sight Machine via the SDK, the first step is always to initialize will point to the name of the tenant on Sight Machine. For example, if you access Sight Machine at the URL *mycompany*.sightmachine.io, then *mycompany* is the name of the tenant you will use. For purposes of this Quick Start documentation, we will use demo as the tenant name. -To initialize a Client: +To initialize a Client: ``` from smsdk import client @@ -37,6 +37,26 @@ tenant = 'demo' cli = client.Client(tenant) ``` +#### Nested Path Support + +For deployments where Sight Machine is hosted under a nested path (e.g., `https://tenant.sightmachine.io/nested/one/two/`), you can specify the base path in two ways: + +**Method 1: Explicit base_path parameter** +```python +cli = client.Client('demo', base_path='/nested/one/two') +``` + +**Method 2: Include path in tenant URL** +```python +cli = client.Client('https://demo.sightmachine.io/nested/one/two') +``` + +The SDK will automatically construct URLs with the correct path prefix for all API calls. For example: +- Without path: `https://demo.sightmachine.io/v1/datatab/cycle` +- With path: `https://demo.sightmachine.io/nested/one/two/v1/datatab/cycle` + +**Note:** The `base_path` parameter takes precedence if both methods are used. + ### Authenticating Sight Machine currently supports two methods of authentication via the SDK: diff --git a/smsdk/Auth/auth.py b/smsdk/Auth/auth.py index 4eef376..5f1353a 100644 --- a/smsdk/Auth/auth.py +++ b/smsdk/Auth/auth.py @@ -48,6 +48,7 @@ def __init__(self, client): client.tenant, client.config["site.domain"], client.config["port"], + client.config.get("base.path"), ) self.session.headers = default_headers() diff --git a/smsdk/client.py b/smsdk/client.py index 42a9db6..556486a 100644 --- a/smsdk/client.py +++ b/smsdk/client.py @@ -6,6 +6,7 @@ import pandas as pd import numpy as np +import typing as t_ try: # for newer pandas versions >1.X @@ -128,7 +129,11 @@ class Client(ClientV0): """Connection point to the Sight Machine platform to retrieve data""" def __init__( - self, tenant: str, site_domain: str = "sightmachine.io", protocol: str = "https" + self, + tenant: str, + site_domain: str = "sightmachine.io", + protocol: str = "https", + base_path: t_.Optional[str] = None, ): """ Initialize the client. @@ -139,9 +144,15 @@ def __init__( The site domain to connect to. Necessary to change if deploying in a non-standard environment. :type site_domain: :class:`string` + :param protocol: Protocol to use (https or http). + :type protocol: :class:`string` + :param base_path: Optional path prefix for nested deployments (e.g., "/nested/one/two") + :type base_path: :class:`string` or None """ - super().__init__(tenant, site_domain=site_domain, protocol=protocol) + super().__init__( + tenant, site_domain=site_domain, protocol=protocol, base_path=base_path + ) @version_check_decorator def select_db_schema(self, schema_name): @@ -176,6 +187,7 @@ def get_data_v1(self, ename, util_name, normalize=True, *args, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) df = pd.DataFrame() @@ -292,6 +304,7 @@ def get_kpis(self, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) return kpis(self.session, base_url).get_kpis(**kwargs) @@ -345,6 +358,7 @@ def get_kpis_for_asset(self, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) if "machine_type" in kwargs["asset_selection"]: # updating kwargs with machine_type's system name in case of user provides display name. @@ -394,6 +408,7 @@ def get_kpi_data_viz( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) if "asset_selection" in kwargs and "machine_type" in kwargs["asset_selection"]: @@ -419,6 +434,7 @@ def get_type_from_machine(self, machine_source=None, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) return machine(self.session, base_url).get_type_from_machine_name( machine_source, **kwargs @@ -440,6 +456,7 @@ def get_machine_schema( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) fields = machineType(self.session, base_url).get_fields(machine_type, **kwargs) fields = [ @@ -479,6 +496,7 @@ def get_fields_of_machine_type( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) fields = machineType(self.session, base_url).get_fields(machine_type, **kwargs) fields = [ @@ -505,6 +523,7 @@ def get_cookbooks(self, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) return cookbook(self.session, base_url).get_cookbooks(**kwargs) @@ -522,6 +541,7 @@ def get_cookbook_top_results(self, recipe_group_id=None, limit=10, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) return cookbook(self.session, base_url).get_top_results( recipe_group_id, limit, **kwargs @@ -541,6 +561,7 @@ def get_cookbook_current_value(self, variables=[], minutes=1440, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) return cookbook(self.session, base_url).get_current_value( variables, minutes, **kwargs @@ -582,6 +603,7 @@ def get_lines(self, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) return lines(self.session, base_url).get_lines(**kwargs) @@ -613,6 +635,7 @@ def get_line_data( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) asset_selection = [] @@ -664,6 +687,7 @@ def get_line_data_lineviz( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) if i_vars: @@ -707,6 +731,7 @@ def create_share_link( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) if assets and model == "cycle" or assets and model == "kpi": machine_types = [] @@ -851,6 +876,7 @@ def get_raw_data( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) select = [{"name": field} for field in fields] kwargs["asset_selection"] = {"raw_data_table": raw_data_table} diff --git a/smsdk/client_v0.py b/smsdk/client_v0.py index 7c7e1cd..d18fcd1 100644 --- a/smsdk/client_v0.py +++ b/smsdk/client_v0.py @@ -81,9 +81,9 @@ def convert_to_valid_url( input_url: str, default_domain: str = "sightmachine.io", default_protocol: str = "https", -): +) -> t_.Tuple[str, t_.Optional[str]]: port = "" - path = "" + path = None # Check if the input URL has a protocol specified if "://" in input_url: @@ -100,9 +100,14 @@ def convert_to_valid_url( if len(parts) == 1: domain = parts[0] - path = "" + path = None else: domain, path = parts + # Only keep path if it's not empty after stripping + if path and path.strip(): + path = "/" + path.rstrip("/") + else: + path = None # Check if the domain has a port specified splits = domain.split(":", 1) @@ -125,11 +130,7 @@ def convert_to_valid_url( valid_url = f"{valid_url}:{port}" # log.warning(f"Ignored the user specified port.") - if path: - # valid_url = f"{valid_url}/{path}" - log.warning(f"Ignored the user specified path.") - - return valid_url + return valid_url, path # We don't have a downtime schema, so hard code one @@ -164,7 +165,11 @@ class ClientV0(object): config = {} def __init__( - self, tenant: str, site_domain: str = "sightmachine.io", protocol: str = "https" + self, + tenant: str, + site_domain: str = "sightmachine.io", + protocol: str = "https", + base_path: t_.Optional[str] = None, ): """ Initialize the client. @@ -175,17 +180,22 @@ def __init__( The site domain to connect to. Necessary to change if deploying in a non-standard environment. :type site_domain: :class:`string` + :param protocol: Protocol to use (https or http). + :type protocol: :class:`string` + :param base_path: Optional path prefix for nested deployments (e.g., "/nested/one/two") + :type base_path: :class:`string` or None """ port = None + extracted_path = None if tenant: - # Convert the input tenant into a valid url - url = convert_to_valid_url( + # Convert the input tenant into a valid url and extract any path + url_without_path, extracted_path = convert_to_valid_url( tenant, default_domain=site_domain, default_protocol=protocol ) - # Parse the input string - parsed_uri = urlparse(url) + # Parse the input string (now without path) + parsed_uri = urlparse(url_without_path) tenant = parsed_uri.netloc.split(".", 1)[0] protocol = parsed_uri.scheme @@ -194,8 +204,16 @@ def __init__( # Extract port port = parsed_uri.port + # Determine final base_path: explicit param takes precedence over extracted + final_base_path = base_path if base_path is not None else extracted_path + self.tenant = tenant - self.config = {"protocol": protocol, "site.domain": site_domain, "port": port} + self.config = { + "protocol": protocol, + "site.domain": site_domain, + "port": port, + "base.path": final_base_path, + } # Setup Authenticator self.auth = Authenticator(self) @@ -280,6 +298,7 @@ def get_data(self, ename, util_name, normalize=True, *args, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) df = pd.DataFrame() @@ -1492,6 +1511,7 @@ def get_cycle_count( self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) cls = smsdkentities.get("dataviz_cycle")(self.session, base_url) @@ -1606,6 +1626,7 @@ def get_part_count(self, start_time="", end_time="", part_type=None, **kwargs): self.tenant, self.config["site.domain"], self.config["port"], + self.config.get("base.path"), ) cls = smsdkentities.get("dataviz_part")(self.session, base_url) diff --git a/smsdk/utils.py b/smsdk/utils.py index 146cd9f..1182308 100644 --- a/smsdk/utils.py +++ b/smsdk/utils.py @@ -21,7 +21,11 @@ def all(self) -> t_.Dict[str, t_.Callable[..., t_.Any]]: def get_url( - protocol: str, tenant: str, site_domain: str, port: t_.Optional[int] = None + protocol: str, + tenant: str, + site_domain: str, + port: t_.Optional[int] = None, + base_path: t_.Optional[str] = None, ) -> str: """ Get the URL of the web address. @@ -34,6 +38,8 @@ def get_url( :type site_domain: :class:`string` :param port: The port number (defaults to None). :type port: :Int + :param base_path: Optional path prefix for nested deployments (e.g., "/nested/one/two") + :type base_path: :class:`string` or None """ url = "" @@ -43,4 +49,13 @@ def get_url( else: url = f"{protocol}://{tenant}.{site_domain}" + # Add base_path if provided + if base_path: + # Normalize: ensure starts with / but doesn't end with / + normalized_path = base_path.strip() + if not normalized_path.startswith("/"): + normalized_path = "/" + normalized_path + normalized_path = normalized_path.rstrip("/") + url = f"{url}{normalized_path}" + return url diff --git a/tests/Uri/test_uri.py b/tests/Uri/test_uri.py index 2267cb2..044092c 100644 --- a/tests/Uri/test_uri.py +++ b/tests/Uri/test_uri.py @@ -120,3 +120,161 @@ def test_create_client_uri_special_cases() -> None: assert ( cli.config["site.domain"] == "localnet.sightmachine.io" ), "Site domain should be set to localnet" + + +def test_create_client_with_explicit_base_path() -> None: + """Test explicit base_path parameter""" + tenant = "demo" + cli = client.Client(tenant, base_path="/nested/one/two") + + assert cli.tenant == "demo", "Tenant should be initialized correctly" + assert cli.config["base.path"] == "/nested/one/two", "Base path should be set" + assert cli.config["protocol"] == "https", "Protocol should be set to HTTPS" + assert ( + cli.config["site.domain"] == "sightmachine.io" + ), "Site domain should be set to sightmachine.io" + + # Verify URL construction includes path + from smsdk.utils import get_url + + url = get_url( + cli.config["protocol"], + cli.tenant, + cli.config["site.domain"], + cli.config["port"], + cli.config.get("base.path"), + ) + assert ( + url == "https://demo.sightmachine.io/nested/one/two" + ), "URL should include nested path" + + +def test_create_client_with_url_including_path() -> None: + """Test path extraction from tenant URL""" + tenant = "https://demo.sightmachine.io/nested/one/two" + cli = client.Client(tenant) + + assert cli.tenant == "demo", "Tenant should be extracted correctly" + assert cli.config["base.path"] == "/nested/one/two", "Path should be extracted" + assert cli.config["protocol"] == "https", "Protocol should be set to HTTPS" + assert ( + cli.config["site.domain"] == "sightmachine.io" + ), "Site domain should be set to sightmachine.io" + + # Verify URL construction includes path + from smsdk.utils import get_url + + url = get_url( + cli.config["protocol"], + cli.tenant, + cli.config["site.domain"], + cli.config["port"], + cli.config.get("base.path"), + ) + assert ( + url == "https://demo.sightmachine.io/nested/one/two" + ), "URL should include nested path" + + +def test_create_client_explicit_path_overrides_extracted() -> None: + """Explicit base_path should override extracted path""" + tenant = "https://demo.sightmachine.io/old/path" + cli = client.Client(tenant, base_path="/new/path") + + assert cli.tenant == "demo", "Tenant should be extracted correctly" + assert ( + cli.config["base.path"] == "/new/path" + ), "Explicit path should override extracted path" + assert cli.config["protocol"] == "https", "Protocol should be set to HTTPS" + + # Verify URL construction uses explicit path + from smsdk.utils import get_url + + url = get_url( + cli.config["protocol"], + cli.tenant, + cli.config["site.domain"], + cli.config["port"], + cli.config.get("base.path"), + ) + assert ( + url == "https://demo.sightmachine.io/new/path" + ), "URL should use explicit path" + + +def test_backward_compatibility_no_path() -> None: + """Existing behavior without path should work unchanged""" + tenant = "demo" + cli = client.Client(tenant) + + assert cli.tenant == "demo", "Tenant should be initialized correctly" + assert cli.config.get("base.path") is None, "Base path should be None by default" + assert cli.config["protocol"] == "https", "Protocol should be set to HTTPS" + assert ( + cli.config["site.domain"] == "sightmachine.io" + ), "Site domain should be set to sightmachine.io" + + # Verify URL construction works without path + from smsdk.utils import get_url + + url = get_url( + cli.config["protocol"], + cli.tenant, + cli.config["site.domain"], + cli.config["port"], + cli.config.get("base.path"), + ) + assert url == "https://demo.sightmachine.io", "URL should not include any path" + + +def test_create_client_with_path_and_port() -> None: + """Test base_path with custom port""" + tenant = "http://demo.sightmachine.io:8080/nested/path" + cli = client.Client(tenant) + + assert cli.tenant == "demo", "Tenant should be extracted correctly" + assert cli.config["base.path"] == "/nested/path", "Path should be extracted" + assert cli.config["protocol"] == "http", "Protocol should be set to HTTP" + assert cli.config["port"] == 8080, "Port should be set to 8080" + assert ( + cli.config["site.domain"] == "sightmachine.io" + ), "Site domain should be set to sightmachine.io" + + # Verify URL construction includes both port and path + from smsdk.utils import get_url + + url = get_url( + cli.config["protocol"], + cli.tenant, + cli.config["site.domain"], + cli.config["port"], + cli.config.get("base.path"), + ) + assert ( + url == "http://demo.sightmachine.io:8080/nested/path" + ), "URL should include port and path" + + +def test_create_client_with_trailing_slash_in_path() -> None: + """Test that trailing slashes in paths are normalized""" + tenant = "https://demo.sightmachine.io/nested/path/" + cli = client.Client(tenant) + + assert cli.tenant == "demo", "Tenant should be extracted correctly" + assert ( + cli.config["base.path"] == "/nested/path" + ), "Trailing slash should be removed" + + # Verify URL construction normalizes path + from smsdk.utils import get_url + + url = get_url( + cli.config["protocol"], + cli.tenant, + cli.config["site.domain"], + cli.config["port"], + cli.config.get("base.path"), + ) + assert ( + url == "https://demo.sightmachine.io/nested/path" + ), "URL should have normalized path" diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py new file mode 100644 index 0000000..ae16944 --- /dev/null +++ b/tests/test_url_utils.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# coding: utf-8 +"""Unit tests for URL utility functions""" + +import pytest +from smsdk.utils import get_url +from smsdk.client_v0 import convert_to_valid_url + + +class TestGetUrl: + """Test cases for get_url() function""" + + def test_get_url_without_path(self): + """Existing behavior - no path""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path=None, + ) + assert url == "https://demo.sightmachine.io" + + def test_get_url_with_path(self): + """New behavior - with path""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="/nested/one/two", + ) + assert url == "https://demo.sightmachine.io/nested/one/two" + + def test_get_url_with_path_trailing_slash_removed(self): + """Path normalization - trailing slash removed""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="/nested/one/two/", + ) + assert url == "https://demo.sightmachine.io/nested/one/two" + + def test_get_url_with_path_leading_slash_added(self): + """Path normalization - leading slash added""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="nested/one/two", + ) + assert url == "https://demo.sightmachine.io/nested/one/two" + + def test_get_url_with_path_both_slashes_normalized(self): + """Path normalization - both leading and trailing""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="nested/one/two/", + ) + assert url == "https://demo.sightmachine.io/nested/one/two" + + def test_get_url_with_port_and_path(self): + """Port and path together""" + url = get_url( + protocol="http", + tenant="demo", + site_domain="sightmachine.io", + port=8080, + base_path="/nested", + ) + assert url == "http://demo.sightmachine.io:8080/nested" + + def test_get_url_with_port_no_path(self): + """Port without path - existing behavior""" + url = get_url( + protocol="http", + tenant="demo", + site_domain="sightmachine.io", + port=8080, + base_path=None, + ) + assert url == "http://demo.sightmachine.io:8080" + + def test_get_url_with_empty_string_path(self): + """Empty string path treated as no path""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="", + ) + assert url == "https://demo.sightmachine.io" + + def test_get_url_with_whitespace_path(self): + """Whitespace-only path treated as no path""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path=" ", + ) + assert url == "https://demo.sightmachine.io" + + def test_get_url_with_single_level_path(self): + """Single level nested path""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="/nested", + ) + assert url == "https://demo.sightmachine.io/nested" + + def test_get_url_with_deep_nested_path(self): + """Deep nested path""" + url = get_url( + protocol="https", + tenant="demo", + site_domain="sightmachine.io", + port=None, + base_path="/a/b/c/d/e", + ) + assert url == "https://demo.sightmachine.io/a/b/c/d/e" + + +class TestConvertToValidUrl: + """Test cases for convert_to_valid_url() function""" + + def test_convert_to_valid_url_extracts_path(self): + """Extract path from input URL""" + url, path = convert_to_valid_url("https://demo.sightmachine.io/nested/one/two") + assert url == "https://demo.sightmachine.io" + assert path == "/nested/one/two" + + def test_convert_to_valid_url_no_path(self): + """No path in URL""" + url, path = convert_to_valid_url("demo") + assert url == "https://demo.sightmachine.io" + assert path is None + + def test_convert_to_valid_url_with_protocol_no_path(self): + """Full URL without path""" + url, path = convert_to_valid_url("https://demo.sightmachine.io") + assert url == "https://demo.sightmachine.io" + assert path is None + + def test_convert_to_valid_url_extracts_trailing_slash_normalized(self): + """Extract path with trailing slash - normalized""" + url, path = convert_to_valid_url("https://demo.sightmachine.io/nested/one/two/") + assert url == "https://demo.sightmachine.io" + assert path == "/nested/one/two" + + def test_convert_to_valid_url_with_port_and_path(self): + """Extract path with port specified""" + url, path = convert_to_valid_url("https://demo.sightmachine.io:8080/nested/path") + assert url == "https://demo.sightmachine.io:8080" + assert path == "/nested/path" + + def test_convert_to_valid_url_simple_tenant_with_custom_domain(self): + """Simple tenant with custom domain""" + url, path = convert_to_valid_url("demo", default_domain="custom.io") + assert url == "https://demo.custom.io" + assert path is None + + def test_convert_to_valid_url_http_protocol(self): + """HTTP protocol instead of HTTPS""" + url, path = convert_to_valid_url("http://demo.sightmachine.io/nested") + assert url == "http://demo.sightmachine.io" + assert path == "/nested" + + def test_convert_to_valid_url_empty_path(self): + """URL with trailing slash but no actual path""" + url, path = convert_to_valid_url("https://demo.sightmachine.io/") + assert url == "https://demo.sightmachine.io" + assert path is None + + def test_convert_to_valid_url_single_level_path(self): + """Single level path extraction""" + url, path = convert_to_valid_url("demo.sightmachine.io/api") + assert url == "https://demo.sightmachine.io" + assert path == "/api" + + def test_convert_to_valid_url_adds_default_domain(self): + """Adds default domain when not present""" + url, path = convert_to_valid_url("demo/nested/path") + assert url == "https://demo.sightmachine.io" + assert path == "/nested/path" From 4533fd1e5a7d9e9bdd5aee1b5b9de7d5a15998fa Mon Sep 17 00:00:00 2001 From: David Freidin Date: Wed, 18 Mar 2026 11:16:10 -0700 Subject: [PATCH 2/2] formatting and type checks --- tests/Uri/test_uri.py | 4 +--- tests/test_url_utils.py | 46 +++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/Uri/test_uri.py b/tests/Uri/test_uri.py index 044092c..193789c 100644 --- a/tests/Uri/test_uri.py +++ b/tests/Uri/test_uri.py @@ -261,9 +261,7 @@ def test_create_client_with_trailing_slash_in_path() -> None: cli = client.Client(tenant) assert cli.tenant == "demo", "Tenant should be extracted correctly" - assert ( - cli.config["base.path"] == "/nested/path" - ), "Trailing slash should be removed" + assert cli.config["base.path"] == "/nested/path", "Trailing slash should be removed" # Verify URL construction normalizes path from smsdk.utils import get_url diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index ae16944..5d01ec7 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -10,7 +10,7 @@ class TestGetUrl: """Test cases for get_url() function""" - def test_get_url_without_path(self): + def test_get_url_without_path(self) -> None: """Existing behavior - no path""" url = get_url( protocol="https", @@ -21,7 +21,7 @@ def test_get_url_without_path(self): ) assert url == "https://demo.sightmachine.io" - def test_get_url_with_path(self): + def test_get_url_with_path(self) -> None: """New behavior - with path""" url = get_url( protocol="https", @@ -32,7 +32,7 @@ def test_get_url_with_path(self): ) assert url == "https://demo.sightmachine.io/nested/one/two" - def test_get_url_with_path_trailing_slash_removed(self): + def test_get_url_with_path_trailing_slash_removed(self) -> None: """Path normalization - trailing slash removed""" url = get_url( protocol="https", @@ -43,7 +43,7 @@ def test_get_url_with_path_trailing_slash_removed(self): ) assert url == "https://demo.sightmachine.io/nested/one/two" - def test_get_url_with_path_leading_slash_added(self): + def test_get_url_with_path_leading_slash_added(self) -> None: """Path normalization - leading slash added""" url = get_url( protocol="https", @@ -54,7 +54,7 @@ def test_get_url_with_path_leading_slash_added(self): ) assert url == "https://demo.sightmachine.io/nested/one/two" - def test_get_url_with_path_both_slashes_normalized(self): + def test_get_url_with_path_both_slashes_normalized(self) -> None: """Path normalization - both leading and trailing""" url = get_url( protocol="https", @@ -65,7 +65,7 @@ def test_get_url_with_path_both_slashes_normalized(self): ) assert url == "https://demo.sightmachine.io/nested/one/two" - def test_get_url_with_port_and_path(self): + def test_get_url_with_port_and_path(self) -> None: """Port and path together""" url = get_url( protocol="http", @@ -76,7 +76,7 @@ def test_get_url_with_port_and_path(self): ) assert url == "http://demo.sightmachine.io:8080/nested" - def test_get_url_with_port_no_path(self): + def test_get_url_with_port_no_path(self) -> None: """Port without path - existing behavior""" url = get_url( protocol="http", @@ -87,7 +87,7 @@ def test_get_url_with_port_no_path(self): ) assert url == "http://demo.sightmachine.io:8080" - def test_get_url_with_empty_string_path(self): + def test_get_url_with_empty_string_path(self) -> None: """Empty string path treated as no path""" url = get_url( protocol="https", @@ -98,7 +98,7 @@ def test_get_url_with_empty_string_path(self): ) assert url == "https://demo.sightmachine.io" - def test_get_url_with_whitespace_path(self): + def test_get_url_with_whitespace_path(self) -> None: """Whitespace-only path treated as no path""" url = get_url( protocol="https", @@ -109,7 +109,7 @@ def test_get_url_with_whitespace_path(self): ) assert url == "https://demo.sightmachine.io" - def test_get_url_with_single_level_path(self): + def test_get_url_with_single_level_path(self) -> None: """Single level nested path""" url = get_url( protocol="https", @@ -120,7 +120,7 @@ def test_get_url_with_single_level_path(self): ) assert url == "https://demo.sightmachine.io/nested" - def test_get_url_with_deep_nested_path(self): + def test_get_url_with_deep_nested_path(self) -> None: """Deep nested path""" url = get_url( protocol="https", @@ -135,61 +135,63 @@ def test_get_url_with_deep_nested_path(self): class TestConvertToValidUrl: """Test cases for convert_to_valid_url() function""" - def test_convert_to_valid_url_extracts_path(self): + def test_convert_to_valid_url_extracts_path(self) -> None: """Extract path from input URL""" url, path = convert_to_valid_url("https://demo.sightmachine.io/nested/one/two") assert url == "https://demo.sightmachine.io" assert path == "/nested/one/two" - def test_convert_to_valid_url_no_path(self): + def test_convert_to_valid_url_no_path(self) -> None: """No path in URL""" url, path = convert_to_valid_url("demo") assert url == "https://demo.sightmachine.io" assert path is None - def test_convert_to_valid_url_with_protocol_no_path(self): + def test_convert_to_valid_url_with_protocol_no_path(self) -> None: """Full URL without path""" url, path = convert_to_valid_url("https://demo.sightmachine.io") assert url == "https://demo.sightmachine.io" assert path is None - def test_convert_to_valid_url_extracts_trailing_slash_normalized(self): + def test_convert_to_valid_url_extracts_trailing_slash_normalized(self) -> None: """Extract path with trailing slash - normalized""" url, path = convert_to_valid_url("https://demo.sightmachine.io/nested/one/two/") assert url == "https://demo.sightmachine.io" assert path == "/nested/one/two" - def test_convert_to_valid_url_with_port_and_path(self): + def test_convert_to_valid_url_with_port_and_path(self) -> None: """Extract path with port specified""" - url, path = convert_to_valid_url("https://demo.sightmachine.io:8080/nested/path") + url, path = convert_to_valid_url( + "https://demo.sightmachine.io:8080/nested/path" + ) assert url == "https://demo.sightmachine.io:8080" assert path == "/nested/path" - def test_convert_to_valid_url_simple_tenant_with_custom_domain(self): + def test_convert_to_valid_url_simple_tenant_with_custom_domain(self) -> None: """Simple tenant with custom domain""" url, path = convert_to_valid_url("demo", default_domain="custom.io") assert url == "https://demo.custom.io" assert path is None - def test_convert_to_valid_url_http_protocol(self): + def test_convert_to_valid_url_http_protocol(self) -> None: """HTTP protocol instead of HTTPS""" url, path = convert_to_valid_url("http://demo.sightmachine.io/nested") assert url == "http://demo.sightmachine.io" assert path == "/nested" - def test_convert_to_valid_url_empty_path(self): + def test_convert_to_valid_url_empty_path(self) -> None: """URL with trailing slash but no actual path""" url, path = convert_to_valid_url("https://demo.sightmachine.io/") assert url == "https://demo.sightmachine.io" assert path is None - def test_convert_to_valid_url_single_level_path(self): + def test_convert_to_valid_url_single_level_path(self) -> None: """Single level path extraction""" url, path = convert_to_valid_url("demo.sightmachine.io/api") assert url == "https://demo.sightmachine.io" assert path == "/api" - def test_convert_to_valid_url_adds_default_domain(self): + def test_convert_to_valid_url_adds_default_domain(self) -> None: """Adds default domain when not present""" url, path = convert_to_valid_url("demo/nested/path") assert url == "https://demo.sightmachine.io"