Skip to content

Commit 6756d8b

Browse files
Update to use new model generator with type hints
1 parent d9b21f5 commit 6756d8b

File tree

5 files changed

+84
-29
lines changed

5 files changed

+84
-29
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ tzdata
1616
urllib3
1717
aiohttp
1818
pyyaml
19-
clio-api-model-generator==0.2.1
19+
clio-api-model-generator==0.2.2

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name="clio-manage-api-client",
13-
version="0.1.3",
13+
version="0.1.4",
1414
author="Unigrated Partners",
1515
author_email="dev@unigratedpartners.com",
1616
description="Python Client for the Clio Manage API.",

src/clio_manage_python_client/classes/base.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
from typing import Dict, Any, Optional, Type, List, Union, Tuple, get_origin, get_args, Literal
33
from datetime import datetime
44
import typing_inspect
5+
import logging
6+
57
from dataclasses import fields, is_dataclass
68

79
from ..configs import *
810
from ..utils.validate_fields import validate_field_string, build_id_field_string
911

12+
if not logging.getLogger().hasHandlers():
13+
logging.basicConfig(
14+
level=logging.INFO, # default level
15+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
16+
)
17+
18+
logger = logging.getLogger(__name__)
19+
1020
def is_optional_type(field_type):
1121
"""
1222
Check if a given type is Optional.
@@ -20,10 +30,12 @@ class BaseRequest:
2030
def __init__(self, base_url: str, base_path=""):
2131
self.base_url = base_url
2232
self.base_path = base_path
33+
self.logger = logging.getLogger(self.__class__.__name__)
2334

2435
def format_url(self, path: str, query_params: Dict[str, Any] = None) -> str:
2536
return f"{self.base_url}{path}"
26-
37+
38+
@staticmethod
2739
def convert_array_field(field_name: str, value: Any, item_type: Type) -> Any:
2840
"""
2941
Convert and validate array query parameters based on API requirements.
@@ -33,6 +45,7 @@ def convert_array_field(field_name: str, value: Any, item_type: Type) -> Any:
3345
:param item_type: The expected type of the array elements.
3446
:return: A validated and converted value based on array size and field name.
3547
"""
48+
logger = logging.getLogger("BaseRequest.convert_array_field")
3649
try:
3750
# Ensure the value is treated as a list
3851
if not isinstance(value, list):
@@ -74,7 +87,10 @@ def convert_array_field(field_name: str, value: Any, item_type: Type) -> Any:
7487

7588
# Default behavior for fields without double underscores
7689
return converted_array
90+
7791
except (ValueError, TypeError) as e:
92+
93+
logger.error("Invalid value for array field '%s': %s", field_name, e)
7894
return {"error": f"Invalid value for array field '{field_name}': {e}"}
7995

8096
def validate_and_convert(self, field_name: str, field_type: Type, value: Any, required: bool) -> Any:
@@ -88,6 +104,8 @@ def validate_and_convert(self, field_name: str, field_type: Type, value: Any, re
88104

89105
# Skip validation for optional fields with None value
90106
if not required and value is None:
107+
print("Required field '%s' not provided", field_name)
108+
self.logger.error("Required field '%s' not provided", field_name)
91109
return None
92110

93111
if value is not None:
@@ -195,7 +213,9 @@ def validate_and_convert(self, field_name: str, field_type: Type, value: Any, re
195213
return bool(value)
196214
elif field_type == str:
197215
return str(value)
198-
except (ValueError, TypeError):
216+
217+
except (ValueError, TypeError) as e:
218+
self.logger.error("Validation error for field '%s': %s", field_name, e)
199219
return {"error": f"Invalid value for field '{field_name}': Expected {field_type}, got {type(value).__name__}."}
200220

201221
return None
@@ -214,7 +234,7 @@ def process_model(model: Type, data: Dict[str, Any]) -> Tuple[Dict[str, Any], Di
214234
errors = {}
215235

216236
for field_name, field_type in model.__annotations__.items():
217-
print(field_name, field_type)
237+
# print(field_name, field_type)
218238
# Use the provided field name for validation; apply mappings after validation
219239
original_field_name = field_name
220240
mapped_field_name = MAPPINGS.get(field_name, field_name)
@@ -227,7 +247,11 @@ def process_model(model: Type, data: Dict[str, Any]) -> Tuple[Dict[str, Any], Di
227247

228248
if value is None:
229249
if not is_optional:
230-
errors[original_field_name] = f"'{original_field_name}' is required but not provided."
250+
msg = f"'{original_field_name}' is required but not provided."
251+
self.logger.error(msg)
252+
errors[original_field_name] = msg
253+
# raise ValueError(msg)
254+
231255
continue
232256

233257
# Process nested models

src/clio_manage_python_client/client.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@
99
from .utils import RateMonitor
1010

1111
class Client:
12+
if TYPE_CHECKING:
13+
get: "GetRequest"
14+
post: "PostRequest"
15+
create: "PostRequest"
16+
update: "PatchRequest"
17+
patch: "PatchRequest"
18+
delete: "DeleteRequest"
19+
download: "DownloadRequest"
20+
all: "AllRequests"
21+
22+
request_methods = {
23+
"get": Get,
24+
"post": Post,
25+
"create": Post,
26+
"patch": Patch,
27+
"delete": Delete,
28+
"download": Download,
29+
"all": All, # For auto paginating record retrieval
30+
"put": Put # Unused
31+
}
32+
1233
def __init__(self,
1334
access_token: str,
1435
region: str = "US",
@@ -44,22 +65,9 @@ def __init__(self,
4465
# Initialize RateMonitor with optional default_limit
4566
self.rate_limiter = RateMonitor(**({"default_limit": default_rate_limit} if default_rate_limit is not None else {}))
4667

47-
# List of HTTP methods and their corresponding handler classes
48-
self.request_methods = REQUEST_METHODS
49-
50-
request_handler_classes = {
51-
"get": Get,
52-
"post": Post,
53-
"patch": Patch,
54-
"delete": Delete,
55-
"download": Download,
56-
"all": All, # For auto paginating record retrieval
57-
"put": Put # Unused
58-
}
59-
6068
# Dynamically initialize and set request handlers as attributes
61-
for method in self.request_methods:
62-
handler_class = request_handler_classes[method]
69+
for method in self.request_methods.keys():
70+
handler_class = self.request_methods[method]
6371
setattr(self, method, handler_class(self.base_url, self.request_handler))
6472

6573
def __getattr__(self, item: str):
@@ -72,10 +80,11 @@ def __getattr__(self, item: str):
7280
return getattr(handler, item)
7381
raise AttributeError(f"'Client' object has no attribute '{item}'")
7482

75-
# Syncronous Requests
83+
# Synchronous Requests
7684
def _make_request(self, api_url: str, method: str, params: dict = None, payload: dict = None, return_all=False):
7785
"""
7886
Makes the actual HTTP request based on the method.
87+
Handles empty or non-JSON responses safely.
7988
"""
8089
headers = {
8190
"Authorization": f"Bearer {self.access_token}",
@@ -93,18 +102,32 @@ def _make_request(self, api_url: str, method: str, params: dict = None, payload:
93102
json=payload,
94103
)
95104
response.raise_for_status()
96-
return response.json(), response
97-
105+
106+
# Safely handle JSON or empty responses
107+
if response.content:
108+
try:
109+
data = response.json()
110+
except ValueError:
111+
# Response is not JSON
112+
data = response.text
113+
else:
114+
data = None # No content
115+
116+
return data, response
117+
98118
except requests.exceptions.RequestException as e:
99-
raise RuntimeError(f"HTTP request failed: {e}") from e
119+
# Provide more info including raw response text if available
120+
resp_text = getattr(e.response, "text", "") if hasattr(e, "response") else ""
121+
raise RuntimeError(f"HTTP request failed: {e}. Response body: {resp_text}") from e
122+
100123

101124
def _request_handler(self, api_url: str, method: str, params: dict = None, payload: dict = None, return_all=False, **kwargs):
102125
"""
103126
Handles requests, including support for paginated responses when return_all=True.
104127
"""
105128
endpoint = api_url.split(self.base_url)[-1].split("?")[0] # Extract endpoint from URL
106129
params = params or {}
107-
# print(f'Return ALl: {return_all}')
130+
108131
@self.rate_limiter(endpoint)
109132
def make_request():
110133
try:

src/clio_manage_python_client/configs.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from pathlib import Path
2+
from typing import TYPE_CHECKING
23

3-
DEFAULT_MODEL_DIRECTORY = Path(__file__).resolve().parent / "models"
4-
REQUEST_METHODS = ["get", "put", "post", "patch", "delete", "download", "all"]
4+
if TYPE_CHECKING:
5+
from .models.method_hints import (
6+
Get as GetRequest,
7+
Post as PostRequest,
8+
Patch as PatchRequest,
9+
Delete as DeleteRequest,
10+
Download as DownloadRequest,
11+
All as AllRequests
12+
)
513

614
MAPPINGS = {
715
"X_API_VERSION": "X-API-VERSION",
@@ -24,4 +32,4 @@
2432
"eu": "https://eu.app.clio.com"
2533
}
2634

27-
API_VERSION_PATH = {4: "api/v4"}
35+
API_VERSION_PATH = {4: "api/v4"}

0 commit comments

Comments
 (0)