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
7 changes: 3 additions & 4 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

title: ""
labels: ""
assignees: ""
---

**Describe the bug**
Expand Down
7 changes: 3 additions & 4 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

title: ""
labels: ""
assignees: ""
---

**Is your feature request related to a problem? Please describe.**
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
We appreciate your interest in improving the Werk24 Python Client!

## Getting Started

- Fork this repository.
- Create a feature branch: `git checkout -b feature-name`.
- Install dependencies: `pip install -r requirements.txt`.

## Testing

- Ensure all tests run successfully:
```bash
pytest
```

## Pull Requests

- Describe the motivation and impact of your changes.
- Reference any relevant issues.
- The team will review your contribution as soon as possible.
Expand Down
10 changes: 5 additions & 5 deletions examples/ask_validation_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class InvalidAsk(BaseModel):
Werk24Client.validate_asks(asks)
print("✓ All ask types are valid!")
except BadRequestException as e:
print(f"✗ Validation failed:")
print("✗ Validation failed:")
print(f" {e}")

print()
Expand All @@ -77,7 +77,7 @@ async def example_empty_asks():
Werk24Client.validate_asks(asks)
print("✓ All ask types are valid!")
except BadRequestException as e:
print(f"✗ Validation failed:")
print("✗ Validation failed:")
print(f" {e}")

print()
Expand Down Expand Up @@ -106,7 +106,7 @@ class InvalidAsk(BaseModel):
async for message in client.read_drawing(drawing, [InvalidAsk()]):
print(f"Received message: {message}")
except BadRequestException as e:
print(f"✓ Validation caught the error before sending to API:")
print("✓ Validation caught the error before sending to API:")
print(f" {e}")
except Exception as e:
print(f"Other error: {e}")
Expand Down Expand Up @@ -135,8 +135,8 @@ class InvalidAsk2(BaseModel):
except BadRequestException as e:
error_msg = str(e)
print("Error message includes:")
print(f" - Invalid ask types: WRONG_TYPE_1, WRONG_TYPE_2")
print(f" - List of all valid ask types")
print(" - Invalid ask types: WRONG_TYPE_1, WRONG_TYPE_2")
print(" - List of all valid ask types")
print()
print("Full error message (truncated):")
print(f" {error_msg[:200]}...")
Expand Down
7 changes: 3 additions & 4 deletions examples/exception_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
W24RateLimitError,
W24ServerError,
W24ValidationError,
Werk24Client,
)


Expand Down Expand Up @@ -155,7 +154,7 @@ def example_error_handling_with_retry():
except W24RateLimitError as e:
retry_count += 1
if retry_count >= max_retries:
print(f"Max retries reached. Giving up.")
print("Max retries reached. Giving up.")
raise

wait_time = e.retry_after or 5
Expand All @@ -168,7 +167,7 @@ def example_error_handling_with_retry():
if e.is_transient:
retry_count += 1
if retry_count >= max_retries:
print(f"Max retries reached. Giving up.")
print("Max retries reached. Giving up.")
raise

wait_time = e.error_details.get("retry_after", 5)
Expand All @@ -178,7 +177,7 @@ def example_error_handling_with_retry():
time.sleep(wait_time)
else:
# Non-transient error, don't retry
print(f"Permanent server error. Not retrying.")
print("Permanent server error. Not retrying.")
raise


Expand Down
89 changes: 89 additions & 0 deletions examples/websocket_connection_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Example demonstrating WebSocket connection management features.

This example shows how to use the enhanced WebSocket connection management
features including heartbeat, auto-reconnect, and graceful shutdown.
"""

import asyncio

from werk24 import Werk24Client
from werk24.models.v2.asks import AskBalloons


async def main():
"""
Demonstrate WebSocket connection management with custom configuration.
"""
# Create client with custom connection management settings
client = Werk24Client(
token="your_token_here",
region="eu-central-1",
ping_interval=30.0, # Send ping every 30 seconds (built-in websockets feature)
ping_timeout=10.0, # Wait 10 seconds for pong response
max_reconnect_attempts=3, # Try to reconnect up to 3 times
reconnect_delay=1.0, # Initial delay of 1 second, with exponential backoff
)

# The client automatically manages the WebSocket connection
async with client:
# Connection is established with retry logic
# Ping/pong heartbeat is handled automatically by websockets library

# Read a drawing - connection will auto-reconnect if it drops
with open("path/to/drawing.pdf", "rb") as f:
drawing_bytes = f.read()

asks = [AskBalloons()]

async for message in client.read_drawing(drawing_bytes, asks):
print(f"Received: {message.message_type}")

# If connection drops during processing, it will automatically
# reconnect and continue receiving messages

# When exiting the context, graceful shutdown is performed:
# WebSocket connection is closed cleanly


async def example_with_default_settings():
"""
Use default connection management settings.
"""
# Default settings:
# - ping_interval: 30.0 seconds
# - ping_timeout: 10.0 seconds
# - max_reconnect_attempts: 3
# - reconnect_delay: 1.0 second (with exponential backoff)

client = Werk24Client(
token="your_token_here",
region="eu-central-1",
)

async with client:
# Connection management happens automatically
pass


async def example_aggressive_reconnect():
"""
Configure aggressive reconnection for unstable networks.
"""
client = Werk24Client(
token="your_token_here",
region="eu-central-1",
ping_interval=10.0, # More frequent heartbeats
ping_timeout=5.0, # Shorter timeout
max_reconnect_attempts=5, # More retry attempts
reconnect_delay=0.5, # Faster initial retry
)

async with client:
# Will reconnect more aggressively if connection drops
pass


if __name__ == "__main__":
# Run the main example
asyncio.run(main())
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ changelog = "https://github.com/W24-Service-GmbH/werk24-python/releases"

[project.scripts]
werk24 = "werk24.cli.werk24:app"

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
6 changes: 3 additions & 3 deletions test_pydantic_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ class InvalidAsk(BaseModel):

try:
request = TechreadRequest(asks=[InvalidAsk()], max_pages=5)
print(f"✗ Invalid ask was accepted (shouldn't happen)")
print("✗ Invalid ask was accepted (shouldn't happen)")
except ValidationError as e:
print(f"✓ Pydantic caught the invalid ask:")
print("✓ Pydantic caught the invalid ask:")
print(f" {e.errors()[0]['msg']}")

print()
Expand All @@ -37,6 +37,6 @@ class InvalidAsk(BaseModel):
print("Test 3: Empty asks list")
try:
request = TechreadRequest(asks=[], max_pages=5)
print(f"✓ Empty asks list accepted (Pydantic doesn't validate list length)")
print("✓ Empty asks list accepted (Pydantic doesn't validate list length)")
except ValidationError as e:
print(f"✗ Validation error: {e}")
1 change: 0 additions & 1 deletion tests/test_ask_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,3 @@ def test_postprocessor_slot_enum_value():
def test_postprocessor_slot_invalid_value():
with pytest.raises(ValueError):
AskCustom(custom_id="foo", postprocessor_slot="red")

2 changes: 1 addition & 1 deletion tests/test_ask_validation_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

import io
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, patch

import pytest

Expand Down
10 changes: 10 additions & 0 deletions tests/test_cli_status_command.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from typer.testing import CliRunner

from werk24 import SystemStatus
Expand All @@ -15,7 +16,16 @@ async def mock_get_status():
)


@pytest.mark.filterwarnings("ignore::ResourceWarning")
def test_status_cli(monkeypatch):
"""Test the status CLI command.

Note: ResourceWarning is suppressed because asyncio.run() (used in the CLI command)
creates and properly closes an event loop, but pytest's garbage collector timing
can trigger a false positive warning. The event loop is correctly managed by
asyncio.run() - this is a known pytest + asyncio.run() interaction issue.
See: https://github.com/pytest-dev/pytest/issues/5502
"""
monkeypatch.setattr(status_cmd.Werk24Client, "get_system_status", mock_get_status)
runner = CliRunner()
result = runner.invoke(status_cmd.app, [])
Expand Down
15 changes: 4 additions & 11 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import os
import uuid
from unittest.mock import AsyncMock, Mock

import pytest
from unittest.mock import AsyncMock, Mock

from werk24 import (
AskMetaData,
Expand All @@ -25,10 +25,7 @@
)

requires_license = pytest.mark.skipif(
not (
os.getenv("W24TECHREAD_AUTH_TOKEN")
and os.getenv("W24TECHREAD_AUTH_REGION")
),
not (os.getenv("W24TECHREAD_AUTH_TOKEN") and os.getenv("W24TECHREAD_AUTH_REGION")),
reason="Werk24 license credentials not provided",
)

Expand Down Expand Up @@ -77,9 +74,7 @@ async def test_read_drawing_with_hooks(drawing_bytes):
await client.read_drawing_with_hooks(drawing_bytes, hooks)

hook.assert_called_once()
assert (
hook.call_args.args[0].message_type == TechreadMessageType.ASK
)
assert hook.call_args.args[0].message_type == TechreadMessageType.ASK


@requires_license
Expand Down Expand Up @@ -144,9 +139,7 @@ def test_get_hook_function_for_message_no_match():
message_type=TechreadMessageType.PROGRESS,
message_subtype=TechreadMessageSubtype.PROGRESS_STARTED,
)
assert (
Werk24Client._get_hook_function_for_message(message, [hook]) is None
)
assert Werk24Client._get_hook_function_for_message(message, [hook]) is None


@pytest.mark.asyncio
Expand Down
9 changes: 7 additions & 2 deletions tests/test_exception_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@


def test_configuration_incorrect_enum_present():
assert TechreadExceptionType.CONFIGURATION_INCORRECT.value == "CONFIGURATION_INCORRECT"
assert W24TechreadExceptionType.CONFIGURATION_INCORRECT.value == "CONFIGURATION_INCORRECT"
assert (
TechreadExceptionType.CONFIGURATION_INCORRECT.value == "CONFIGURATION_INCORRECT"
)
assert (
W24TechreadExceptionType.CONFIGURATION_INCORRECT.value
== "CONFIGURATION_INCORRECT"
)
2 changes: 0 additions & 2 deletions tests/test_new_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
they properly handle error details and provide useful information.
"""

import pytest

from werk24.utils.exceptions import (
W24AuthenticationError,
W24RateLimitError,
Expand Down
Loading
Loading