Skip to content

Commit 92a64f2

Browse files
committed
Phase 9: Main Client Class - Complete implementation
✅ Main Client Class Implementation: - Created pathao/client.py with PathaoClient class - Unified API interface for all Pathao services - Flexible credential management (parameters + environment variables) - Environment support (sandbox/production) with automatic URL configuration - Complete service module integration (stores, orders, locations, prices) ✅ Features: - Credential loading from parameters or environment variables with precedence - Environment validation (sandbox/production) with proper base URL setting - Shared HTTP client and authentication across all modules - Public methods for token management (get_access_token, refresh_token, is_token_valid) - Comprehensive credential validation with ConfigurationError ✅ Service Integration: - Store management module (client.stores) - Order management module (client.orders) - Location services module (client.locations) - Price calculation module (client.prices) - Consistent initialization and dependency injection ✅ Test Coverage: - Created tests/test_client.py with 11 comprehensive tests - All initialization scenarios covered (parameters, environment, mixed) - Environment validation and credential handling tested - Module initialization and dependency injection verified - 100% test pass rate (11/11 tests passing) ✅ Code Quality: - Applied Black formatting for consistent code style - Fixed flake8 linting issues (line length) - Clean, minimal implementation following best practices ✅ Progress: 9/17 phases complete (52.9%) - All 194 tests passing across entire codebase - Updated implementation checklist with completion status
1 parent 314ad35 commit 92a64f2

3 files changed

Lines changed: 333 additions & 21 deletions

File tree

pathao/client.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Main client class for Pathao Python SDK."""
2+
3+
import os
4+
from typing import Optional
5+
6+
from .exceptions import ConfigurationError
7+
from .http_client import HTTPClient
8+
from .modules.auth import AuthModule
9+
from .modules.store import StoreModule
10+
from .modules.order import OrderModule
11+
from .modules.location import LocationModule
12+
from .modules.price import PriceModule
13+
14+
15+
class PathaoClient:
16+
"""Main client for Pathao API."""
17+
18+
def __init__(
19+
self,
20+
client_id: Optional[str] = None,
21+
client_secret: Optional[str] = None,
22+
username: Optional[str] = None,
23+
password: Optional[str] = None,
24+
environment: str = "sandbox",
25+
):
26+
"""Initialize Pathao client."""
27+
# Load credentials
28+
credentials = self._load_credentials(
29+
client_id, client_secret, username, password
30+
)
31+
32+
# Validate environment
33+
if environment not in ["sandbox", "production"]:
34+
raise ConfigurationError(
35+
"Invalid environment. Must be 'sandbox' or 'production'",
36+
missing_config="environment",
37+
)
38+
39+
# Set base URL
40+
base_url = (
41+
"https://courier-api.pathao.com"
42+
if environment == "production"
43+
else "https://courier-api-sandbox.pathao.com"
44+
)
45+
46+
# Initialize HTTP client
47+
self.http_client = HTTPClient(base_url)
48+
49+
# Initialize auth module
50+
self.auth_module = AuthModule(self.http_client, credentials)
51+
52+
# Initialize service modules
53+
self.stores = StoreModule(self.http_client, self.auth_module)
54+
self.orders = OrderModule(self.http_client, self.auth_module)
55+
self.locations = LocationModule(self.http_client, self.auth_module)
56+
self.prices = PriceModule(self.http_client, self.auth_module)
57+
58+
def _load_credentials(
59+
self,
60+
client_id: Optional[str],
61+
client_secret: Optional[str],
62+
username: Optional[str],
63+
password: Optional[str],
64+
) -> dict:
65+
"""Load credentials from parameters or environment."""
66+
# Try parameters first, then environment variables
67+
credentials = {
68+
"client_id": client_id or os.getenv("PATHAO_CLIENT_ID"),
69+
"client_secret": client_secret or os.getenv("PATHAO_CLIENT_SECRET"),
70+
"username": username or os.getenv("PATHAO_USERNAME"),
71+
"password": password or os.getenv("PATHAO_PASSWORD"),
72+
}
73+
74+
# Validate required credentials
75+
missing = [k for k, v in credentials.items() if not v]
76+
if missing:
77+
raise ConfigurationError(
78+
f"Missing required credentials: {', '.join(missing)}",
79+
missing_config=missing[0],
80+
)
81+
82+
return credentials
83+
84+
def get_access_token(self) -> str:
85+
"""Get current access token."""
86+
return self.auth_module.get_access_token()
87+
88+
def refresh_token(self) -> None:
89+
"""Refresh access token."""
90+
self.auth_module.refresh_token()
91+
92+
def is_token_valid(self) -> bool:
93+
"""Check if current token is valid."""
94+
return self.auth_module.is_token_valid()

pathao_implementation_checklist.md

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -564,30 +564,83 @@
564564

565565
---
566566

567-
## Phase 9: Main Client Class
567+
## Phase 9: Main Client Class
568568

569569
### 9.1 PathaoClient Implementation
570570

571-
- [ ] **client.py**
572-
- [ ] `PathaoClient` class
573-
- [ ] `__init__()` method
574-
- [ ] Load credentials from parameters or .env
575-
- [ ] Validate environment (sandbox/production)
576-
- [ ] Initialize HTTPClient
577-
- [ ] Initialize AuthModule
578-
- [ ] Validate credentials
579-
- [ ] Properties:
580-
- [ ] `stores` → StoreModule
581-
- [ ] `orders` → OrderModule
582-
- [ ] `locations` → LocationModule
583-
- [ ] `prices` → PriceModule
584-
- [ ] Public methods:
585-
- [ ] `get_access_token() -> str`
586-
- [ ] `refresh_token() -> None`
587-
- [ ] `is_token_valid() -> bool`
588-
- [ ] Private methods:
589-
- [ ] `_load_credentials_from_env() -> dict`
590-
- [ ] `_validate_credentials() -> None`
571+
- [x] **client.py**
572+
- [x] `PathaoClient` class
573+
- [x] `__init__()` method
574+
- [x] Load credentials from parameters or environment variables
575+
- [x] Validate environment (sandbox/production)
576+
- [x] Initialize HTTPClient with correct base URL
577+
- [x] Initialize AuthModule with credentials
578+
- [x] Initialize all service modules (stores, orders, locations, prices)
579+
- [x] Validate all required credentials
580+
- [x] Properties and methods:
581+
- [x] `stores` → StoreModule instance
582+
- [x] `orders` → OrderModule instance
583+
- [x] `locations` → LocationModule instance
584+
- [x] `prices` → PriceModule instance
585+
- [x] Public methods:
586+
- [x] `get_access_token() -> str`
587+
- [x] `refresh_token() -> None`
588+
- [x] `is_token_valid() -> bool`
589+
- [x] Private methods:
590+
- [x] `_load_credentials()` from parameters or environment
591+
592+
**Credential Management:**
593+
- [x] Support for parameter-based credentials
594+
- [x] Support for environment variable credentials
595+
- [x] Mixed credential sources (parameters override environment)
596+
- [x] Comprehensive validation with ConfigurationError
597+
598+
**Environment Support:**
599+
- [x] Sandbox environment (https://courier-api-sandbox.pathao.com)
600+
- [x] Production environment (https://courier-api.pathao.com)
601+
- [x] Environment validation with proper error handling
602+
603+
**Test coverage:**
604+
- [x] Initialization with parameters
605+
- [x] Initialization with environment variables
606+
- [x] Mixed credential sources
607+
- [x] Environment validation (sandbox/production)
608+
- [x] Missing credentials handling
609+
- [x] Module initialization verification
610+
- [x] Public method delegation
611+
612+
### 9.2 Main Client Features
613+
614+
- [x] **Unified API interface**
615+
- [x] Single entry point for all Pathao services
616+
- [x] Consistent initialization across all modules
617+
- [x] Shared HTTP client and authentication
618+
- [x] Clean separation of concerns
619+
620+
- [x] **Flexible credential management**
621+
- [x] Parameter-based credentials for direct usage
622+
- [x] Environment variable support for deployment
623+
- [x] Mixed sources with parameter precedence
624+
- [x] Comprehensive validation and error reporting
625+
626+
- [x] **Environment management**
627+
- [x] Sandbox environment for testing
628+
- [x] Production environment for live usage
629+
- [x] Automatic base URL configuration
630+
- [x] Environment validation
631+
632+
- [x] **Service module integration**
633+
- [x] Store management (create, list, get)
634+
- [x] Order management (create, bulk, track)
635+
- [x] Location services (cities, zones, areas)
636+
- [x] Price calculation (delivery pricing)
637+
638+
- [x] **Comprehensive test suite (11 tests, 100% pass)**
639+
- [x] Initialization scenarios (parameters, environment, mixed)
640+
- [x] Environment validation and URL configuration
641+
- [x] Credential validation and error handling
642+
- [x] Module initialization and dependency injection
643+
- [x] Public method delegation to auth module() -> None`
591644
- [ ] `_get_base_url() -> str`
592645
- [ ] `_initialize_modules() -> None`
593646

tests/test_client.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Tests for main client class."""
2+
3+
import pytest
4+
from unittest.mock import Mock, patch
5+
6+
from pathao.client import PathaoClient
7+
from pathao.exceptions import ConfigurationError
8+
9+
10+
class TestPathaoClient:
11+
"""Test cases for PathaoClient."""
12+
13+
def test_init_with_parameters(self):
14+
"""Test client initialization with parameters."""
15+
client = PathaoClient(
16+
client_id="test_id",
17+
client_secret="test_secret",
18+
username="test_user",
19+
password="test_pass",
20+
environment="sandbox",
21+
)
22+
23+
assert client.http_client.base_url == "https://courier-api-sandbox.pathao.com"
24+
assert hasattr(client, "stores")
25+
assert hasattr(client, "orders")
26+
assert hasattr(client, "locations")
27+
assert hasattr(client, "prices")
28+
29+
def test_init_production_environment(self):
30+
"""Test client initialization with production environment."""
31+
client = PathaoClient(
32+
client_id="test_id",
33+
client_secret="test_secret",
34+
username="test_user",
35+
password="test_pass",
36+
environment="production",
37+
)
38+
39+
assert client.http_client.base_url == "https://courier-api.pathao.com"
40+
41+
def test_init_invalid_environment(self):
42+
"""Test client initialization with invalid environment."""
43+
with pytest.raises(ConfigurationError) as exc_info:
44+
PathaoClient(
45+
client_id="test_id",
46+
client_secret="test_secret",
47+
username="test_user",
48+
password="test_pass",
49+
environment="invalid",
50+
)
51+
assert "Invalid environment" in str(exc_info.value)
52+
53+
@patch.dict(
54+
"os.environ",
55+
{
56+
"PATHAO_CLIENT_ID": "env_id",
57+
"PATHAO_CLIENT_SECRET": "env_secret",
58+
"PATHAO_USERNAME": "env_user",
59+
"PATHAO_PASSWORD": "env_pass",
60+
},
61+
)
62+
def test_init_with_environment_variables(self):
63+
"""Test client initialization with environment variables."""
64+
client = PathaoClient()
65+
66+
assert client.auth_module.credentials["client_id"] == "env_id"
67+
assert client.auth_module.credentials["username"] == "env_user"
68+
69+
def test_init_missing_credentials(self):
70+
"""Test client initialization with missing credentials."""
71+
with pytest.raises(ConfigurationError) as exc_info:
72+
PathaoClient()
73+
assert "Missing required credentials" in str(exc_info.value)
74+
75+
def test_init_partial_credentials(self):
76+
"""Test client initialization with partial credentials."""
77+
with pytest.raises(ConfigurationError) as exc_info:
78+
PathaoClient(client_id="test_id", username="test_user")
79+
assert "client_secret" in str(exc_info.value) or "password" in str(
80+
exc_info.value
81+
)
82+
83+
def test_get_access_token(self):
84+
"""Test get access token method."""
85+
client = PathaoClient(
86+
client_id="test_id",
87+
client_secret="test_secret",
88+
username="test_user",
89+
password="test_pass",
90+
)
91+
92+
# Mock the auth module
93+
client.auth_module.get_access_token = Mock(return_value="test_token")
94+
95+
token = client.get_access_token()
96+
assert token == "test_token"
97+
client.auth_module.get_access_token.assert_called_once()
98+
99+
def test_refresh_token(self):
100+
"""Test refresh token method."""
101+
client = PathaoClient(
102+
client_id="test_id",
103+
client_secret="test_secret",
104+
username="test_user",
105+
password="test_pass",
106+
)
107+
108+
# Mock the auth module
109+
client.auth_module.refresh_token = Mock()
110+
111+
client.refresh_token()
112+
client.auth_module.refresh_token.assert_called_once()
113+
114+
def test_is_token_valid(self):
115+
"""Test is token valid method."""
116+
client = PathaoClient(
117+
client_id="test_id",
118+
client_secret="test_secret",
119+
username="test_user",
120+
password="test_pass",
121+
)
122+
123+
# Mock the auth module
124+
client.auth_module.is_token_valid = Mock(return_value=True)
125+
126+
is_valid = client.is_token_valid()
127+
assert is_valid is True
128+
client.auth_module.is_token_valid.assert_called_once()
129+
130+
def test_module_initialization(self):
131+
"""Test that all modules are properly initialized."""
132+
client = PathaoClient(
133+
client_id="test_id",
134+
client_secret="test_secret",
135+
username="test_user",
136+
password="test_pass",
137+
)
138+
139+
# Check that all modules are initialized with correct dependencies
140+
assert client.stores.http_client == client.http_client
141+
assert client.stores.auth_module == client.auth_module
142+
143+
assert client.orders.http_client == client.http_client
144+
assert client.orders.auth_module == client.auth_module
145+
146+
assert client.locations.http_client == client.http_client
147+
assert client.locations.auth_module == client.auth_module
148+
149+
assert client.prices.http_client == client.http_client
150+
assert client.prices.auth_module == client.auth_module
151+
152+
@patch.dict(
153+
"os.environ",
154+
{
155+
"PATHAO_CLIENT_ID": "env_id",
156+
"PATHAO_CLIENT_SECRET": "env_secret",
157+
"PATHAO_USERNAME": "env_user",
158+
},
159+
)
160+
def test_init_mixed_credentials(self):
161+
"""Test client initialization with mixed credentials."""
162+
client = PathaoClient(password="param_pass")
163+
164+
assert client.auth_module.credentials["client_id"] == "env_id"
165+
assert client.auth_module.credentials["password"] == "param_pass"

0 commit comments

Comments
 (0)