Skip to content

Commit 45ac6d9

Browse files
committed
Improves error handling and logging
Enhances error handling by introducing custom exception classes for API related issues and invalid parameters. Replaces generic ValueError exceptions with more specific InvalidParameterError, APIConnectionError, APITimeoutError, APIAuthenticationError and APIResponseError exceptions. Adds logging to provide more context when exceptions are raised, making debugging easier.
1 parent bf020bc commit 45ac6d9

7 files changed

Lines changed: 230 additions & 140 deletions

File tree

pydeepskylog/contrast_reserve.py

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import math
33
from typing import Optional, Tuple, List
44
from pydeepskylog.config import ContrastReserveConfig
5-
5+
from pydeepskylog.exceptions import InvalidParameterError
66

77
def surface_brightness(magnitude: float, object_diameter1: float, object_diameter2: float) -> float:
88
"""
@@ -11,21 +11,28 @@ def surface_brightness(magnitude: float, object_diameter1: float, object_diamete
1111
:param object_diameter1: The diameter along the major axis of the object in arc seconds
1212
:param object_diameter2: The diameter along the minor axis of the object in arc seconds
1313
:return: The surface brightness of the object in magnitudes per square arc second
14-
:raises ValueError: If any parameter is not a number or if object diameters are not positive
14+
:raises InvalidParameterError: If any parameter is not a number or if object diameters are not positive
1515
"""
1616
# Validate inputs
17+
logger: logging = logging.getLogger(__name__)
18+
1719
if not isinstance(magnitude, (int, float)):
18-
raise ValueError("Magnitude must be a number")
20+
logger.error(f"The magnitude parameter is not a number")
21+
raise InvalidParameterError("Magnitude must be a number")
1922
if not isinstance(object_diameter1, (int, float)):
20-
raise ValueError("Object diameter 1 must be a number")
23+
logger.error(f"The object diameter 1 parameter is not a number")
24+
raise InvalidParameterError("Object diameter 1 must be a number")
2125
if not isinstance(object_diameter2, (int, float)):
22-
raise ValueError("Object diameter 2 must be a number")
26+
logger.error(f"The object diameter 2 parameter is not a number")
27+
raise InvalidParameterError("Object diameter 2 must be a number")
2328

2429
# Check for positive diameters
2530
if object_diameter1 <= 0:
26-
raise ValueError("Object diameter 1 must be positive")
31+
logger.error(f"The object diameter 1 parameter is not positive: {object_diameter1}")
32+
raise InvalidParameterError("Object diameter 1 must be positive")
2733
if object_diameter2 <= 0:
28-
raise ValueError("Object diameter 2 must be positive")
34+
logger.error(f"The object diameter 2 parameter is not positive: {object_diameter2}")
35+
raise InvalidParameterError("Object diameter 2 must be positive")
2936

3037
return magnitude + (2.5 * math.log10(2827.0 * (object_diameter1 / 60) * (object_diameter2 / 60)))
3138

@@ -45,42 +52,55 @@ def validate_contrast_reserve_inputs(
4552
:param magnitude: The magnitude of the object to observe
4653
:param object_diameter1: The diameter along the major axis of the object in arc seconds
4754
:param object_diameter2: The diameter along the minor axis of the object in arc seconds
48-
:raises ValueError: If parameters have invalid types or values
55+
:raises InvalidParameterError: If parameters have invalid types or values
4956
"""
5057
# Validate required numeric inputs
58+
logger: logging = logging.getLogger(__name__)
59+
5160
if not isinstance(sqm, (int, float)):
52-
raise ValueError("SQM must be a number")
61+
logger.error(f"The sqm parameter is not a number")
62+
raise InvalidParameterError("SQM must be a number")
5363
if not isinstance(telescope_diameter, (int, float)):
54-
raise ValueError("Telescope diameter must be a number")
64+
logger.error(f"The telescope diameter parameter is not a number")
65+
raise InvalidParameterError("Telescope diameter must be a number")
5566
if not isinstance(magnification, (int, float)):
56-
raise ValueError("Magnification must be a number")
67+
logger.error(f"The magnification parameter is not a number")
68+
raise InvalidParameterError("Magnification must be a number")
5769

5870
# Check for positive values
5971
if telescope_diameter <= 0:
60-
raise ValueError("Telescope diameter must be positive")
72+
logger.error(f"The telescope diameter parameter is not positive: {telescope_diameter}")
73+
raise InvalidParameterError("Telescope diameter must be positive")
6174
if magnification <= 0:
62-
raise ValueError("Magnification must be positive")
75+
logger.error(f"The magnification parameter is not positive: {magnification}")
76+
raise InvalidParameterError("Magnification must be positive")
6377

6478
# Validate surf_brightness if provided
6579
if surf_brightness is not None and not isinstance(surf_brightness, (int, float)):
66-
raise ValueError("Surface brightness must be a number or None")
80+
logger.error(f"The surface brightness parameter is not a number: {surf_brightness}")
81+
raise InvalidParameterError("Surface brightness must be a number or None")
6782

6883
# Validate magnitude if provided and needed
6984
if surf_brightness is None and magnitude is not None:
7085
if not isinstance(magnitude, (int, float)):
71-
raise ValueError("Magnitude must be a number or None")
86+
logger.error(f"The magnitude parameter is not a number: {magnitude}")
87+
raise InvalidParameterError("Magnitude must be a number or None")
7288

7389
# Validate object diameters if provided
7490
if object_diameter1 is not None and not isinstance(object_diameter1, (int, float)):
75-
raise ValueError("Object diameter 1 must be a number or None")
91+
logger.error(f"The object diameter parameter is not a number: {object_diameter1}")
92+
raise InvalidParameterError("Object diameter 1 must be a number or None")
7693
if object_diameter2 is not None and not isinstance(object_diameter2, (int, float)):
77-
raise ValueError("Object diameter 2 must be a number or None")
94+
logger.error(f"The object diameter parameter is not a number: {object_diameter2}")
95+
raise InvalidParameterError("Object diameter 2 must be a number or None")
7896

7997
# Check for positive diameters if provided
8098
if object_diameter1 is not None and object_diameter1 <= 0:
81-
raise ValueError("Object diameter 1 must be positive")
99+
logger.error(f"The object diameter 1 parameter is not positive: {object_diameter1}")
100+
raise InvalidParameterError("Object diameter 1 must be positive")
82101
if object_diameter2 is not None and object_diameter2 <= 0:
83-
raise ValueError("Object diameter 2 must be positive")
102+
logger.error(f"The object diameter 2 parameter is not positive: {object_diameter2}")
103+
raise InvalidParameterError("Object diameter 2 must be positive")
84104

85105

86106
def calculate_initial_parameters(
@@ -96,8 +116,8 @@ def calculate_initial_parameters(
96116
:param object_diameter2: The diameter along the minor axis of the object in arc seconds
97117
:return: A tuple containing (aperture_in_inches, sbb1, object_diameter1_in_arc_minutes, object_diameter2_in_arc_minutes)
98118
"""
99-
logger = logging.getLogger()
100-
119+
logger: logging = logging.getLogger(__name__)
120+
101121
aperture_in_inches = telescope_diameter / 25.4
102122

103123
# Minimum useful magnification
@@ -106,7 +126,7 @@ def calculate_initial_parameters(
106126
if object_diameter1 is None or object_diameter2 is None:
107127
# If the object diameters are not given, we cannot calculate the contrast reserve
108128
logger.error("Cannot calculate contrast reserve, missing object diameters")
109-
raise ValueError("Object diameters must be provided to calculate contrast reserve")
129+
raise InvalidParameterError("Object diameters must be provided to calculate contrast reserve")
110130

111131
object_diameter1_in_arc_minutes = object_diameter1 / 60.0
112132
object_diameter2_in_arc_minutes = object_diameter2 / 60.0
@@ -133,8 +153,8 @@ def calculate_log_object_contrast(
133153
:param object_diameter2: The diameter along the minor axis of the object in arc seconds
134154
:return: The log object contrast
135155
"""
136-
logger = logging.getLogger()
137-
156+
logger: logging = logging.getLogger(__name__)
157+
138158
if surf_brightness:
139159
# If the surface brightness is given, use it to calculate the log object contrast
140160
# Convert surf_brightness to magnitudes per square arc second
@@ -144,7 +164,7 @@ def calculate_log_object_contrast(
144164
if magnitude is None:
145165
# If not, we cannot calculate the log object contrast
146166
logger.error("Cannot calculate log object contrast, missing parameters")
147-
raise ValueError("Magnitude must be provided if surface brightness is not given")
167+
raise InvalidParameterError("Magnitude must be provided if surface brightness is not given")
148168
log_object_contrast = -0.4 * (surface_brightness(magnitude, object_diameter1, object_diameter2) - sqm)
149169

150170
return log_object_contrast
@@ -259,10 +279,10 @@ def contrast_reserve(
259279
:param object_diameter2: The diameter along the minor axis of the object in arc seconds
260280
261281
:return: The contrast reserve of the object
262-
:raises ValueError: If parameters have invalid types or values
282+
:raises InvalidParameterError: If parameters have invalid types or values
263283
"""
264284
# Log a string using python logger
265-
logger = logging.getLogger()
285+
logger: logging = logging.getLogger(__name__)
266286
logger.info("Calculating the contrast reserve")
267287

268288
# Validate inputs
@@ -276,8 +296,7 @@ def contrast_reserve(
276296
aperture_in_inches, sbb1, object_diameter1_in_arc_minutes, object_diameter2_in_arc_minutes = calculate_initial_parameters(
277297
sqm, telescope_diameter, object_diameter1, object_diameter2
278298
)
279-
except ValueError as e:
280-
logger.error(str(e))
299+
except InvalidParameterError as e:
281300
raise
282301

283302
# Calculate log object contrast
@@ -318,46 +337,60 @@ def optimal_detection_magnification(
318337
:raises ValueError: If parameters have invalid types or values
319338
"""
320339
# Validate required numeric inputs
340+
logger: logging = logging.getLogger(__name__)
341+
321342
if not isinstance(sqm, (int, float)):
322-
raise ValueError("SQM must be a number")
343+
logger.error(f"The sqm parameter is not a number: {sqm}")
344+
raise InvalidParameterError("SQM must be a number")
323345
if not isinstance(telescope_diameter, (int, float)):
324-
raise ValueError("Telescope diameter must be a number")
346+
logger.error(f"The telescope diameter parameter is not a number: {telescope_diameter}")
347+
raise InvalidParameterError("Telescope diameter must be a number")
325348

326349
# Check for positive telescope diameter
327350
if telescope_diameter <= 0:
328-
raise ValueError("Telescope diameter must be positive")
351+
logger.error(f"The telescope diameter parameter is not positive: {telescope_diameter}")
352+
raise InvalidParameterError("Telescope diameter must be positive")
329353

330354
# Validate surf_brightness if provided
331355
if surf_brightness is not None and not isinstance(surf_brightness, (int, float)):
332-
raise ValueError("Surface brightness must be a number or None")
356+
logger.error(f"The surface brightness parameter is not a number: {surf_brightness}")
357+
raise InvalidParameterError("Surface brightness must be a number or None")
333358

334359
# Validate magnitude if provided and needed
335360
if surf_brightness is None and magnitude is not None:
336361
if not isinstance(magnitude, (int, float)):
337-
raise ValueError("Magnitude must be a number or None")
362+
logger.error(f"The magnitude parameter is not a number: {magnitude}")
363+
raise InvalidParameterError("Magnitude must be a number or None")
338364

339365
# Validate object diameters if provided
340366
if object_diameter1 is not None and not isinstance(object_diameter1, (int, float)):
341-
raise ValueError("Object diameter 1 must be a number or None")
367+
logger.error(f"The object diameter 1 parameter is not a number: {object_diameter1}")
368+
raise InvalidParameterError("Object diameter 1 must be a number or None")
342369
if object_diameter2 is not None and not isinstance(object_diameter2, (int, float)):
343-
raise ValueError("Object diameter 2 must be a number or None")
370+
logger.error(f"The object diameter 2 parameter is not a number: {object_diameter2}")
371+
raise InvalidParameterError("Object diameter 2 must be a number or None")
344372

345373
# Check for positive diameters if provided
346374
if object_diameter1 is not None and object_diameter1 <= 0:
347-
raise ValueError("Object diameter 1 must be positive")
375+
logger.error(f"The object diameter 1 parameter is not positive: {object_diameter1}")
376+
raise InvalidParameterError("Object diameter 1 must be positive")
348377
if object_diameter2 is not None and object_diameter2 <= 0:
349-
raise ValueError("Object diameter 2 must be positive")
378+
logger.error(f"The object diameter 2 parameter is not positive: {object_diameter2}")
379+
raise InvalidParameterError("Object diameter 2 must be positive")
350380

351381
# Validate magnifications list
352382
if not isinstance(magnifications, list):
353-
raise ValueError("Magnifications must be a list")
383+
logger.error("Magnifications parameter is not a list")
384+
raise InvalidParameterError("Magnifications must be a list")
354385

355386
# Validate each magnification in the list
356387
for mag in magnifications:
357388
if not isinstance(mag, (int, float)):
358-
raise ValueError("Each magnification must be a number")
389+
logger.error(f"Each magnification must be a number: {mag}")
390+
raise InvalidParameterError("Each magnification must be a number")
359391
if mag <= 0:
360-
raise ValueError("Each magnification must be positive")
392+
logger.error(f"Each magnification must be positive: {mag}")
393+
raise InvalidParameterError("Each magnification must be positive")
361394

362395
best_contrast = -999
363396
best_x = 0

pydeepskylog/deepskylog_interface.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import logging
12
import requests
23
import time
34
from typing import Dict, List, Any, Optional
4-
5+
from pydeepskylog.exceptions import (
6+
APIConnectionError, APITimeoutError, APIAuthenticationError, APIResponseError, InvalidParameterError
7+
)
58
DSL_API_BASE_URL: str = "https://test.deepskylog.org/api/" # Change this as needed
69

710
# Simple in-memory cache: {url: (timestamp, data)}
@@ -158,6 +161,7 @@ def _dsl_api_call(api_call: str, username: str) -> Dict[str, Any]:
158161
"""
159162
api_url: str = f"{DSL_API_BASE_URL}{api_call}/{username}"
160163
now: float = time.time()
164+
logger: logging = logging.getLogger(__name__)
161165

162166
# Check cache
163167
cache_entry = _DSL_API_CACHE.get(api_url)
@@ -169,33 +173,42 @@ def _dsl_api_call(api_call: str, username: str) -> Dict[str, Any]:
169173
try:
170174
response = requests.get(api_url, timeout=10)
171175
if response.status_code in (401, 403):
172-
raise PermissionError(f"Authentication failed for user '{username}' (status {response.status_code})")
176+
logger.error(f"Authentication failed for user '{username}' (status {response.status_code})")
177+
raise APIAuthenticationError(f"Authentication failed for user '{username}' (status {response.status_code})")
173178
response.raise_for_status()
174179
try:
175180
data = response.json()
176181
except ValueError:
177-
raise RuntimeError("Failed to decode JSON response from DeepskyLog API")
182+
logger.error("Failed to decode JSON response from DeepskyLog API")
183+
raise APIResponseError("Failed to decode JSON response from DeepskyLog API")
178184
# Validate that the response is a dict or list
179185
if not isinstance(data, (dict, list)):
180-
raise RuntimeError("Unexpected JSON structure: expected dict or list")
186+
logger.error("Unexpected JSON structure: expected dict or list")
187+
raise APIResponseError("Unexpected JSON structure: expected dict or list")
181188

182189
# Further validation: check for required fields based on api_call
183190
if api_call in ("instrument", "eyepieces", "lenses", "filters"):
184191
if not data:
185-
raise RuntimeError(f"No data returned for {api_call}")
192+
logger.error(f"No data returned for {api_call}")
193+
raise APIResponseError(f"No data returned for {api_call}")
186194
# Optionally, check for expected keys in the first item
187195
sample = next(iter(data.values()), None) if isinstance(data, dict) else data[0]
188196
if not isinstance(sample, dict):
189-
raise RuntimeError(f"Malformed data for {api_call}: expected dict entries")
197+
logger.error(f"Malformed data for {api_call}: expected dict entries")
198+
raise APIResponseError(f"Malformed data for {api_call}: expected dict entries")
190199
# Store in cache
191200
_DSL_API_CACHE[api_url] = (now, data)
192201
return data
193202

194203
except requests.exceptions.ConnectionError:
195-
raise ConnectionError("Failed to connect to DeepskyLog API server")
204+
logger.error("Failed to connect to DeepskyLog API server")
205+
raise APIConnectionError("Failed to connect to DeepskyLog API server")
196206
except requests.exceptions.Timeout:
197-
raise ConnectionError("Request to DeepskyLog API timed out")
207+
logger.error("Request to DeepskyLog API timed out")
208+
raise APITimeoutError("Request to DeepskyLog API timed out")
198209
except requests.exceptions.HTTPError as e:
199-
raise RuntimeError(f"HTTP error occurred: {e}")
210+
logger.error(f"HTTP error occurred: {e}")
211+
raise APIResponseError(f"HTTP error occurred: {e}")
200212
except requests.exceptions.RequestException as e:
201-
raise RuntimeError(f"An error occurred during the API request: {e}")
213+
logger.error(f"An error occurred during the API request: {e}")
214+
raise APIResponseError(f"An error occurred during the API request: {e}")

pydeepskylog/exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# pydeepskylog/exceptions.py
2+
class PyDeepSkyLogError(Exception):
3+
"""Base exception for pydeepskylog errors."""
4+
5+
class APIConnectionError(PyDeepSkyLogError):
6+
"""Raised when API connection fails."""
7+
8+
class APITimeoutError(PyDeepSkyLogError):
9+
"""Raised when API request times out."""
10+
11+
class APIAuthenticationError(PyDeepSkyLogError):
12+
"""Raised when API authentication fails."""
13+
14+
class APIResponseError(PyDeepSkyLogError):
15+
"""Raised for invalid or unexpected API responses."""
16+
17+
class InvalidParameterError(PyDeepSkyLogError):
18+
"""Raised for invalid function parameters."""

0 commit comments

Comments
 (0)