Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["wheel", "setuptools"]

[project]
name = "werk24"
version = "2.4.4"
version = "2.5.0"
description = "AI-powered platform for extracting and analyzing data from technical drawings / CAD drawings to enhance manufacturing workflows."
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE.txt" }
Expand Down
19 changes: 15 additions & 4 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,23 +93,34 @@ async def test_read_drawing_with_callback(
@pytest.mark.asyncio
async def test_invalid_token():
"""
Test that an empty token raises an UnauthorizedException.
Test that a bogus token raises an UnauthorizedException.
"""
with pytest.raises(UnauthorizedException):
async with Werk24Client(token="", region="eu-central-1"):
async with Werk24Client(token="not-a-valid-token", region="eu-central-1"):
...


@pytest.mark.asyncio
async def test_invalid_region():
async def test_empty_token():
"""
Test that an empty region raises an InvalidLicenseException.
Test that an empty token raises an InvalidLicenseException.
"""
with pytest.raises(InvalidLicenseException):
async with Werk24Client(token="", region=None):
...


@pytest.mark.asyncio
async def test_token_only_license():
"""
Test that a client can be created with a token and no region, as issued
during registration.
"""
client = Werk24Client(token="some-token")
assert client.license.token == "some-token" # nosec
assert client.license.region is None # nosec


def test_run_preflight_checks_invalid_type():
with pytest.raises(UnsupportedMediaType):
Werk24Client.run_preflight_checks("not-bytes")
Expand Down
21 changes: 21 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"W24TECHREAD_AUTH_TOKEN=valid_token\nW24TECHREAD_AUTH_REGION=valid_region\n"
)
INVALID_LICENSE_TEXT = "INVALID_KEY=missing_token\n"
BARE_TOKEN_TEXT = "valid_token\n"


@pytest.fixture
Expand Down Expand Up @@ -55,6 +56,26 @@ def test_parse_license_text(valid_license):
assert license == valid_license # nosec


def test_parse_license_text_bare_token():
"""Test parsing a bare token (the format issued during registration)."""
license = parse_license_text(BARE_TOKEN_TEXT)
assert license.token == "valid_token" # nosec
assert license.region is None # nosec


def test_parse_license_text_bare_token_with_whitespace():
"""Test parsing a bare token that has surrounding whitespace/blank lines."""
license = parse_license_text("\n valid_token \n\n")
assert license.token == "valid_token" # nosec
assert license.region is None # nosec


def test_parse_license_text_empty():
"""Test parsing empty text raises an exception."""
with pytest.raises(InvalidLicenseException):
parse_license_text(" \n")


def test_parse_license_text_invalid():
"""Test parsing invalid license text raises an exception."""
with pytest.raises(InvalidLicenseException):
Expand Down
39 changes: 22 additions & 17 deletions werk24/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

@app.command()
def init():
"""Initialize Werk24 by providing or creating a license."""
"""Initialize Werk24 by providing or creating a license token."""
try:
find_license()
console.print(
Expand All @@ -30,22 +30,22 @@ def init():
)
return
except InvalidLicenseException:
pass # Continue to ask user to create a license file
pass # Continue to ask user to provide a token
ask_user_to_create_license()


def ask_user_to_create_license():
"""Guide the user to provide or create a license."""
"""Guide the user to provide or create a license token."""
CREATE_A_LICENSE_FILE_TEXT = """
To use Werk24, you need a valid license file.
If you don't have one, you can create a trial license to get started.
To use Werk24, you need a valid token.
If you don't have one, you can sign up to get a license.
"""
console.print(
Panel(Text(CREATE_A_LICENSE_FILE_TEXT, style="bold red"), title="License Setup")
)
console.print("[blue]Choose an option:[/blue]")
console.print("[yellow]1.[/yellow] Provide a license file")
console.print("[yellow]2.[/yellow] Create a trial license")
console.print("[yellow]1.[/yellow] Provide a token")
console.print("[yellow]2.[/yellow] Sign up to get a license")

while True:
try:
Expand All @@ -54,7 +54,7 @@ def ask_user_to_create_license():
accept_license_from_terminal()
break
elif choice == 2:
create_trial_license()
sign_up_for_license()
break
else:
raise ValueError("Invalid choice")
Expand All @@ -63,9 +63,14 @@ def ask_user_to_create_license():


def accept_license_from_terminal():
"""Accept license text from the user."""
"""Accept a token from the user.

The token you receive during registration can be pasted directly. For
backwards compatibility, a legacy license block (``W24TECHREAD_AUTH_TOKEN=...``)
is also accepted.
"""
console.print(
"[blue]Please paste your license text below and press [bold]Enter[/bold] twice when done:[/blue]"
"[blue]Please paste your token below and press [bold]Enter[/bold] twice when done:[/blue]"
)

license_text = ""
Expand All @@ -82,17 +87,17 @@ def accept_license_from_terminal():
try:
license = parse_license_text(license_text)
save_license_file(license)
console.print(Panel("[bold green]License successfully saved![/bold green]"))
console.print(Panel("[bold green]Token successfully saved![/bold green]"))
except InvalidLicenseException:
console.print("[red]Invalid license text. Please try again.[/red]")
console.print("[red]Invalid token. Please try again.[/red]")
accept_license_from_terminal() # Retry on failure


def create_trial_license():
"""Guide the user to create a trial license."""
console.print("[blue]To create a trial license, visit the following URL:[/blue]")
console.print(f"[bold cyan]{settings.trial_license_url}[/bold cyan]")
console.print("[blue]Once you have the license text, paste it below.[/blue]")
def sign_up_for_license():
"""Guide the user to sign up for a license and obtain a token."""
console.print("[blue]To sign up for a license, visit the following URL:[/blue]")
console.print(f"[bold cyan]{settings.signup_url}[/bold cyan]")
console.print("[blue]Once you have your token, paste it below.[/blue]")
accept_license_from_terminal()


Expand Down
6 changes: 3 additions & 3 deletions werk24/utils/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Settings(BaseSettings):
Attributes:
----------
- trial_license_url (HttpUrl): URL to access the trial license.
- signup_url (HttpUrl): URL to sign up and obtain a license token.
Default is "https://werk24.io/trial-license".
- wss_server (AnyUrl): WebSocket server URL for connecting to the Werk24 API.
Default is "wss://ws-api.w24.co/v2".
Expand All @@ -33,8 +33,8 @@ class Settings(BaseSettings):
"""

trial_license_url: HttpUrl = "https://werk24.io/trial-license"
"""URL for obtaining a trial license."""
signup_url: HttpUrl = "https://werk24.io/trial-license"
"""URL for signing up and obtaining a license token."""

http_server: AnyUrl = "https://api.w24.co"
wss_server: AnyUrl = "wss://ws-api.w24.co/v2"
Expand Down
2 changes: 1 addition & 1 deletion werk24/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class InvalidLicenseException(TechreadException):
cli_message_header: str = "Invalid License"
cli_message_body: str = (
"The provided license is invalid or has expired.\n\n"
"Please ensure that you provide a token AND a region."
"Please ensure that you provide a valid token."
)


Expand Down
90 changes: 67 additions & 23 deletions werk24/utils/license.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Optional

import dotenv
from pydantic import BaseModel
from pydantic import BaseModel, field_validator

from werk24.utils.exceptions import InvalidLicenseException

Expand All @@ -20,14 +20,31 @@
# Expand user paths once at import time
SEARCH_PATHS = [os.path.expanduser(p) for p in _RAW_SEARCH_PATHS]

# Name of the environment variable / dotenv key that holds the auth token.
TOKEN_ENV_KEY = "W24TECHREAD_AUTH_TOKEN"

# Name of the environment variable / dotenv key that holds the (legacy) region.
REGION_ENV_KEY = "W24TECHREAD_AUTH_REGION"

# Initialize logger
logger = get_logger()


# Define License Model
class License(BaseModel):
token: str
region: str

# The region is a legacy field. Registration now only issues a token, so the
# region is optional and kept for backwards compatibility with existing
# license files and environment variables.
region: Optional[str] = None

@field_validator("token")
@classmethod
def _token_must_not_be_empty(cls, value: str) -> str:
if not value or not value.strip():
raise ValueError("The license token must not be empty.")
return value.strip()


def find_license(token: Optional[str] = None, region: Optional[str] = None) -> License:
Expand All @@ -48,15 +65,13 @@ def find_license(token: Optional[str] = None, region: Optional[str] = None) -> L
"""

# -----------------------------------------------------------
# Check if token and region are provided
# Check if a token is provided (the region is optional)
# -----------------------------------------------------------
if token is not None:
try:
return License(token=token, region=region)
except ValueError as e:
raise InvalidLicenseException(
"The license requires a token and a region"
) from e
raise InvalidLicenseException("The license requires a valid token") from e

# -----------------------------------------------------------
# If not provided, search for a valid license
Expand Down Expand Up @@ -103,9 +118,9 @@ def find_license_in_envs() -> Optional[License]:
- License: A valid License object if found.
None: If no valid license is found in the environment variables.
"""
token = os.environ.get("W24TECHREAD_AUTH_TOKEN")
region = os.environ.get("W24TECHREAD_AUTH_REGION")
if token and region:
token = os.environ.get(TOKEN_ENV_KEY)
region = os.environ.get(REGION_ENV_KEY)
if token:
logger.debug("License found in environment variables.")
return License(token=token, region=region)
logger.debug("Required environment variables not set.")
Expand Down Expand Up @@ -145,9 +160,17 @@ def parse_license_text(text: str) -> License:
"""
Parse license text and validate its format.

Two formats are supported:

1. A dotenv style block containing ``W24TECHREAD_AUTH_TOKEN`` (and
optionally the legacy ``W24TECHREAD_AUTH_REGION``). This is the format of
older license files.
2. A bare token, as issued during registration. In this case the whole
text is treated as the token.

Args:
----
- text (str): The content of the license file.
- text (str): The content of the license file or the raw token.

Returns:
-------
Expand All @@ -158,17 +181,36 @@ def parse_license_text(text: str) -> License:
- InvalidLicenseException: If the license text is invalid.
"""
logger.debug("Parsing license text...")
try:
vars = dotenv.dotenv_values(stream=io.StringIO(text))
token = vars.get("W24TECHREAD_AUTH_TOKEN")
region = vars.get("W24TECHREAD_AUTH_REGION")
if not (token and region):
raise KeyError("missing token or region")
logger.debug("License text parsed successfully.")
return License(token=token, region=region)
except (ValueError, KeyError) as e:
logger.error(f"Missing key in license text: {e}")
raise InvalidLicenseException("Invalid license text format.") from e

# -----------------------------------------------------------
# Legacy dotenv format: recognised by the presence of the token key.
# -----------------------------------------------------------
if TOKEN_ENV_KEY in text:
try:
vars = dotenv.dotenv_values(stream=io.StringIO(text))
token = vars.get(TOKEN_ENV_KEY)
region = vars.get(REGION_ENV_KEY)
if not token:
raise KeyError("missing token")
logger.debug("License text parsed successfully (dotenv format).")
return License(token=token, region=region)
except (ValueError, KeyError) as e:
logger.error(f"Missing key in license text: {e}")
raise InvalidLicenseException("Invalid license text format.") from e

# -----------------------------------------------------------
# New format: a bare token, as issued during registration. Use the first
# non-empty line so trailing whitespace or blank lines from a copy/paste do
# not break parsing. A line containing "=" is a malformed dotenv block
# rather than a token, and is rejected.
# -----------------------------------------------------------
token = next((line.strip() for line in text.splitlines() if line.strip()), "")
if not token or "=" in token:
logger.error("No valid token found in license text.")
raise InvalidLicenseException("Invalid license text format.")

logger.debug("License text parsed successfully (bare token).")
return License(token=token)


def save_license_file(license: License):
Expand All @@ -182,8 +224,10 @@ def save_license_file(license: License):
license_path = SEARCH_PATHS[0]
try:
with open(license_path, "w+") as file:
file.write(f"W24TECHREAD_AUTH_TOKEN={license.token}\n")
file.write(f"W24TECHREAD_AUTH_REGION={license.region}\n")
file.write(f"{TOKEN_ENV_KEY}={license.token}\n")
# Only persist the region if one is present (legacy licenses).
if license.region:
file.write(f"{REGION_ENV_KEY}={license.region}\n")
logger.info(f"License saved successfully at {license_path}")
except Exception as e:
logger.error(f"Error saving license file: {e}")
Expand Down
Loading