nrl-tracker 1.8.0__py3-none-any.whl → 1.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/METADATA +2 -2
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/RECORD +32 -28
- pytcl/__init__.py +3 -3
- pytcl/assignment_algorithms/dijkstra_min_cost.py +0 -1
- pytcl/assignment_algorithms/network_simplex.py +0 -2
- pytcl/astronomical/ephemerides.py +8 -4
- pytcl/astronomical/relativity.py +20 -0
- pytcl/containers/__init__.py +19 -8
- pytcl/containers/base.py +82 -9
- pytcl/containers/covertree.py +14 -21
- pytcl/containers/kd_tree.py +18 -45
- pytcl/containers/rtree.py +43 -4
- pytcl/containers/vptree.py +14 -21
- pytcl/core/__init__.py +59 -2
- pytcl/core/constants.py +59 -0
- pytcl/core/exceptions.py +865 -0
- pytcl/core/optional_deps.py +531 -0
- pytcl/core/validation.py +4 -6
- pytcl/dynamic_estimation/kalman/matrix_utils.py +427 -0
- pytcl/dynamic_estimation/kalman/square_root.py +20 -213
- pytcl/dynamic_estimation/kalman/sr_ukf.py +5 -5
- pytcl/dynamic_estimation/kalman/types.py +98 -0
- pytcl/mathematical_functions/signal_processing/detection.py +19 -0
- pytcl/mathematical_functions/transforms/wavelets.py +7 -6
- pytcl/plotting/coordinates.py +25 -27
- pytcl/plotting/ellipses.py +14 -16
- pytcl/plotting/metrics.py +7 -5
- pytcl/plotting/tracks.py +8 -7
- pytcl/terrain/loaders.py +10 -6
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional dependencies management for the Tracker Component Library.
|
|
3
|
+
|
|
4
|
+
This module provides a unified system for handling optional dependencies,
|
|
5
|
+
including lazy imports, availability checks, and helpful error messages.
|
|
6
|
+
|
|
7
|
+
The system supports:
|
|
8
|
+
- Lazy imports that only load modules when accessed
|
|
9
|
+
- Availability flags for conditional code paths
|
|
10
|
+
- Decorators for functions requiring optional dependencies
|
|
11
|
+
- Helpful error messages with installation instructions
|
|
12
|
+
|
|
13
|
+
Examples
|
|
14
|
+
--------
|
|
15
|
+
Check if a dependency is available:
|
|
16
|
+
|
|
17
|
+
>>> from pytcl.core.optional_deps import is_available
|
|
18
|
+
>>> if is_available("plotly"):
|
|
19
|
+
... import plotly.graph_objects as go
|
|
20
|
+
|
|
21
|
+
Use a decorator to require a dependency:
|
|
22
|
+
|
|
23
|
+
>>> from pytcl.core.optional_deps import requires
|
|
24
|
+
>>> @requires("plotly", extra="visualization")
|
|
25
|
+
... def create_3d_plot(data):
|
|
26
|
+
... import plotly.graph_objects as go
|
|
27
|
+
... return go.Figure(data)
|
|
28
|
+
|
|
29
|
+
Import with a helpful error on failure:
|
|
30
|
+
|
|
31
|
+
>>> from pytcl.core.optional_deps import import_optional
|
|
32
|
+
>>> go = import_optional("plotly.graph_objects", package="plotly", extra="visualization")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import importlib
|
|
36
|
+
import logging
|
|
37
|
+
from functools import wraps
|
|
38
|
+
from types import ModuleType
|
|
39
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
40
|
+
|
|
41
|
+
from pytcl.core.exceptions import DependencyError
|
|
42
|
+
|
|
43
|
+
# Module logger
|
|
44
|
+
_logger = logging.getLogger("pytcl.core.optional_deps")
|
|
45
|
+
|
|
46
|
+
# Type variable for generic function signatures
|
|
47
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Package Configuration
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
# Mapping of package names to their pip install extras
|
|
55
|
+
# Format: package_name -> (extra_name, pip_package_name)
|
|
56
|
+
PACKAGE_EXTRAS: dict[str, tuple[str, str]] = {
|
|
57
|
+
# Visualization
|
|
58
|
+
"plotly": ("visualization", "plotly"),
|
|
59
|
+
# Astronomy
|
|
60
|
+
"astropy": ("astronomy", "astropy"),
|
|
61
|
+
"jplephem": ("astronomy", "jplephem"),
|
|
62
|
+
# Geodesy
|
|
63
|
+
"pyproj": ("geodesy", "pyproj"),
|
|
64
|
+
"geographiclib": ("geodesy", "geographiclib"),
|
|
65
|
+
# Optimization
|
|
66
|
+
"cvxpy": ("optimization", "cvxpy"),
|
|
67
|
+
# Signal processing
|
|
68
|
+
"pywt": ("signal", "pywavelets"),
|
|
69
|
+
"pywavelets": ("signal", "pywavelets"),
|
|
70
|
+
# Terrain data
|
|
71
|
+
"netCDF4": ("terrain", "netCDF4"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Friendly names for features provided by each package
|
|
75
|
+
PACKAGE_FEATURES: dict[str, str] = {
|
|
76
|
+
"plotly": "interactive visualization",
|
|
77
|
+
"astropy": "astronomical calculations",
|
|
78
|
+
"jplephem": "JPL ephemeris access",
|
|
79
|
+
"pyproj": "coordinate transformations",
|
|
80
|
+
"geographiclib": "geodetic calculations",
|
|
81
|
+
"cvxpy": "convex optimization",
|
|
82
|
+
"pywt": "wavelet transforms",
|
|
83
|
+
"pywavelets": "wavelet transforms",
|
|
84
|
+
"netCDF4": "NetCDF file reading",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# Availability Cache
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
# Cache of package availability checks
|
|
93
|
+
_availability_cache: dict[str, bool] = {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _clear_cache() -> None:
|
|
97
|
+
"""Clear the availability cache. Mainly for testing."""
|
|
98
|
+
_availability_cache.clear()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_available(package: str) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if an optional package is available.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
package : str
|
|
108
|
+
Name of the package to check (e.g., "plotly", "pywt").
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
bool
|
|
113
|
+
True if the package is installed and can be imported.
|
|
114
|
+
|
|
115
|
+
Examples
|
|
116
|
+
--------
|
|
117
|
+
>>> from pytcl.core.optional_deps import is_available
|
|
118
|
+
>>> if is_available("plotly"):
|
|
119
|
+
... from plotly import graph_objects as go
|
|
120
|
+
... # use plotly
|
|
121
|
+
... else:
|
|
122
|
+
... print("Plotly not available")
|
|
123
|
+
|
|
124
|
+
Notes
|
|
125
|
+
-----
|
|
126
|
+
Results are cached for performance. Use ``_clear_cache()`` if you
|
|
127
|
+
need to re-check availability (e.g., after installing a package).
|
|
128
|
+
"""
|
|
129
|
+
if package in _availability_cache:
|
|
130
|
+
return _availability_cache[package]
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
importlib.import_module(package)
|
|
134
|
+
available = True
|
|
135
|
+
_logger.debug("Optional package '%s' is available", package)
|
|
136
|
+
except ImportError:
|
|
137
|
+
available = False
|
|
138
|
+
_logger.debug("Optional package '%s' is not available", package)
|
|
139
|
+
|
|
140
|
+
_availability_cache[package] = available
|
|
141
|
+
return available
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# =============================================================================
|
|
145
|
+
# Import Helpers
|
|
146
|
+
# =============================================================================
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _get_install_command(package: str, extra: Optional[str] = None) -> str:
|
|
150
|
+
"""Generate the pip install command for a package."""
|
|
151
|
+
if extra:
|
|
152
|
+
return f"pip install pytcl[{extra}]"
|
|
153
|
+
|
|
154
|
+
# Check if we know the extra for this package
|
|
155
|
+
if package in PACKAGE_EXTRAS:
|
|
156
|
+
extra_name, _ = PACKAGE_EXTRAS[package]
|
|
157
|
+
return f"pip install pytcl[{extra_name}]"
|
|
158
|
+
|
|
159
|
+
# Default to direct package install
|
|
160
|
+
pip_package = package
|
|
161
|
+
if package in PACKAGE_EXTRAS:
|
|
162
|
+
_, pip_package = PACKAGE_EXTRAS[package]
|
|
163
|
+
return f"pip install {pip_package}"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _get_feature_name(package: str) -> str:
|
|
167
|
+
"""Get a friendly feature name for a package."""
|
|
168
|
+
return PACKAGE_FEATURES.get(package, f"{package} functionality")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def import_optional(
|
|
172
|
+
module_name: str,
|
|
173
|
+
*,
|
|
174
|
+
package: Optional[str] = None,
|
|
175
|
+
extra: Optional[str] = None,
|
|
176
|
+
feature: Optional[str] = None,
|
|
177
|
+
) -> ModuleType:
|
|
178
|
+
"""
|
|
179
|
+
Import an optional module with a helpful error message on failure.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
module_name : str
|
|
184
|
+
Full module path to import (e.g., "plotly.graph_objects").
|
|
185
|
+
package : str, optional
|
|
186
|
+
Package name for error message. If not provided, extracted from
|
|
187
|
+
module_name.
|
|
188
|
+
extra : str, optional
|
|
189
|
+
Name of the pytcl extra that provides this dependency
|
|
190
|
+
(e.g., "visualization", "astronomy").
|
|
191
|
+
feature : str, optional
|
|
192
|
+
Description of the feature requiring this dependency.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
module : ModuleType
|
|
197
|
+
The imported module.
|
|
198
|
+
|
|
199
|
+
Raises
|
|
200
|
+
------
|
|
201
|
+
DependencyError
|
|
202
|
+
If the module cannot be imported.
|
|
203
|
+
|
|
204
|
+
Examples
|
|
205
|
+
--------
|
|
206
|
+
>>> go = import_optional(
|
|
207
|
+
... "plotly.graph_objects",
|
|
208
|
+
... package="plotly",
|
|
209
|
+
... extra="visualization",
|
|
210
|
+
... feature="3D plotting"
|
|
211
|
+
... )
|
|
212
|
+
"""
|
|
213
|
+
if package is None:
|
|
214
|
+
package = module_name.split(".")[0]
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
module = importlib.import_module(module_name)
|
|
218
|
+
_logger.debug("Successfully imported optional module '%s'", module_name)
|
|
219
|
+
return module
|
|
220
|
+
except ImportError as e:
|
|
221
|
+
if feature is None:
|
|
222
|
+
feature = _get_feature_name(package)
|
|
223
|
+
|
|
224
|
+
install_cmd = _get_install_command(package, extra)
|
|
225
|
+
|
|
226
|
+
msg = f"{package} is required for {feature}. " f"Install with: {install_cmd}"
|
|
227
|
+
_logger.warning("Failed to import optional module '%s': %s", module_name, e)
|
|
228
|
+
raise DependencyError(
|
|
229
|
+
msg,
|
|
230
|
+
package=package,
|
|
231
|
+
feature=feature,
|
|
232
|
+
install_command=install_cmd,
|
|
233
|
+
) from e
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# =============================================================================
|
|
237
|
+
# Decorator for Optional Dependencies
|
|
238
|
+
# =============================================================================
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def requires(
|
|
242
|
+
*packages: str,
|
|
243
|
+
extra: Optional[str] = None,
|
|
244
|
+
feature: Optional[str] = None,
|
|
245
|
+
) -> Callable[[F], F]:
|
|
246
|
+
"""
|
|
247
|
+
Decorator to mark a function as requiring optional dependencies.
|
|
248
|
+
|
|
249
|
+
When the decorated function is called, it checks if the required
|
|
250
|
+
packages are available. If not, it raises a DependencyError with
|
|
251
|
+
a helpful message.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
*packages : str
|
|
256
|
+
One or more package names required by the function.
|
|
257
|
+
extra : str, optional
|
|
258
|
+
Name of the pytcl extra that provides these dependencies.
|
|
259
|
+
feature : str, optional
|
|
260
|
+
Description of the feature provided by the function.
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
decorator : callable
|
|
265
|
+
Decorator that wraps the function with dependency checking.
|
|
266
|
+
|
|
267
|
+
Examples
|
|
268
|
+
--------
|
|
269
|
+
>>> from pytcl.core.optional_deps import requires
|
|
270
|
+
>>>
|
|
271
|
+
>>> @requires("plotly", extra="visualization")
|
|
272
|
+
... def create_plot(data):
|
|
273
|
+
... import plotly.graph_objects as go
|
|
274
|
+
... return go.Figure(data)
|
|
275
|
+
>>>
|
|
276
|
+
>>> # This will raise DependencyError if plotly is not installed
|
|
277
|
+
>>> create_plot([1, 2, 3])
|
|
278
|
+
|
|
279
|
+
Multiple packages:
|
|
280
|
+
|
|
281
|
+
>>> @requires("astropy", "jplephem", extra="astronomy")
|
|
282
|
+
... def compute_ephemeris(body, time):
|
|
283
|
+
... from astropy.time import Time
|
|
284
|
+
... import jplephem
|
|
285
|
+
... # ...
|
|
286
|
+
|
|
287
|
+
Notes
|
|
288
|
+
-----
|
|
289
|
+
The decorator checks availability at call time, not at definition
|
|
290
|
+
time. This allows the module to be imported even if the optional
|
|
291
|
+
dependencies are not installed.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def decorator(func: F) -> F:
|
|
295
|
+
@wraps(func)
|
|
296
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
297
|
+
missing = [pkg for pkg in packages if not is_available(pkg)]
|
|
298
|
+
|
|
299
|
+
if missing:
|
|
300
|
+
# Get feature name from first package if not provided
|
|
301
|
+
feat = feature or _get_feature_name(missing[0])
|
|
302
|
+
|
|
303
|
+
if len(missing) == 1:
|
|
304
|
+
pkg_str = missing[0]
|
|
305
|
+
install_cmd = _get_install_command(missing[0], extra)
|
|
306
|
+
else:
|
|
307
|
+
pkg_str = ", ".join(missing)
|
|
308
|
+
install_cmd = _get_install_command(missing[0], extra)
|
|
309
|
+
|
|
310
|
+
msg = (
|
|
311
|
+
f"{pkg_str} {'is' if len(missing) == 1 else 'are'} required "
|
|
312
|
+
f"for {feat}. Install with: {install_cmd}"
|
|
313
|
+
)
|
|
314
|
+
raise DependencyError(
|
|
315
|
+
msg,
|
|
316
|
+
package=missing[0],
|
|
317
|
+
feature=feat,
|
|
318
|
+
install_command=install_cmd,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return func(*args, **kwargs)
|
|
322
|
+
|
|
323
|
+
return wrapper # type: ignore[return-value]
|
|
324
|
+
|
|
325
|
+
return decorator
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# =============================================================================
|
|
329
|
+
# Availability Flags (for backward compatibility)
|
|
330
|
+
# =============================================================================
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# These flags are computed lazily when first accessed
|
|
334
|
+
class _AvailabilityFlags:
|
|
335
|
+
"""Lazy availability flags for common optional packages."""
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def HAS_PLOTLY(self) -> bool:
|
|
339
|
+
"""True if plotly is available."""
|
|
340
|
+
return is_available("plotly")
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def HAS_PYWT(self) -> bool:
|
|
344
|
+
"""True if pywavelets is available."""
|
|
345
|
+
return is_available("pywt")
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def PYWT_AVAILABLE(self) -> bool:
|
|
349
|
+
"""True if pywavelets is available (alias)."""
|
|
350
|
+
return is_available("pywt")
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def HAS_JPLEPHEM(self) -> bool:
|
|
354
|
+
"""True if jplephem is available."""
|
|
355
|
+
return is_available("jplephem")
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def HAS_ASTROPY(self) -> bool:
|
|
359
|
+
"""True if astropy is available."""
|
|
360
|
+
return is_available("astropy")
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def HAS_PYPROJ(self) -> bool:
|
|
364
|
+
"""True if pyproj is available."""
|
|
365
|
+
return is_available("pyproj")
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def HAS_CVXPY(self) -> bool:
|
|
369
|
+
"""True if cvxpy is available."""
|
|
370
|
+
return is_available("cvxpy")
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def HAS_NETCDF4(self) -> bool:
|
|
374
|
+
"""True if netCDF4 is available."""
|
|
375
|
+
return is_available("netCDF4")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# Create singleton instance
|
|
379
|
+
_flags = _AvailabilityFlags()
|
|
380
|
+
|
|
381
|
+
# Export individual flags for convenient access
|
|
382
|
+
HAS_PLOTLY = property(lambda self: _flags.HAS_PLOTLY)
|
|
383
|
+
HAS_PYWT = property(lambda self: _flags.HAS_PYWT)
|
|
384
|
+
PYWT_AVAILABLE = property(lambda self: _flags.PYWT_AVAILABLE)
|
|
385
|
+
HAS_JPLEPHEM = property(lambda self: _flags.HAS_JPLEPHEM)
|
|
386
|
+
HAS_ASTROPY = property(lambda self: _flags.HAS_ASTROPY)
|
|
387
|
+
HAS_PYPROJ = property(lambda self: _flags.HAS_PYPROJ)
|
|
388
|
+
HAS_CVXPY = property(lambda self: _flags.HAS_CVXPY)
|
|
389
|
+
HAS_NETCDF4 = property(lambda self: _flags.HAS_NETCDF4)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# =============================================================================
|
|
393
|
+
# Lazy Module Loader
|
|
394
|
+
# =============================================================================
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class LazyModule:
|
|
398
|
+
"""
|
|
399
|
+
A lazy module loader that imports the module on first access.
|
|
400
|
+
|
|
401
|
+
This allows optional dependencies to be "imported" at module level
|
|
402
|
+
without triggering an import error until they're actually used.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
module_name : str
|
|
407
|
+
Full module path to import.
|
|
408
|
+
package : str, optional
|
|
409
|
+
Package name for error messages.
|
|
410
|
+
extra : str, optional
|
|
411
|
+
pytcl extra that provides this dependency.
|
|
412
|
+
feature : str, optional
|
|
413
|
+
Feature description for error messages.
|
|
414
|
+
|
|
415
|
+
Examples
|
|
416
|
+
--------
|
|
417
|
+
>>> from pytcl.core.optional_deps import LazyModule
|
|
418
|
+
>>> go = LazyModule("plotly.graph_objects", package="plotly")
|
|
419
|
+
>>> # No import yet...
|
|
420
|
+
>>> fig = go.Figure() # Import happens here
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
def __init__(
|
|
424
|
+
self,
|
|
425
|
+
module_name: str,
|
|
426
|
+
*,
|
|
427
|
+
package: Optional[str] = None,
|
|
428
|
+
extra: Optional[str] = None,
|
|
429
|
+
feature: Optional[str] = None,
|
|
430
|
+
):
|
|
431
|
+
self._module_name = module_name
|
|
432
|
+
self._package = package or module_name.split(".")[0]
|
|
433
|
+
self._extra = extra
|
|
434
|
+
self._feature = feature
|
|
435
|
+
self._module: Optional[ModuleType] = None
|
|
436
|
+
|
|
437
|
+
def _load(self) -> ModuleType:
|
|
438
|
+
"""Load the module if not already loaded."""
|
|
439
|
+
if self._module is None:
|
|
440
|
+
self._module = import_optional(
|
|
441
|
+
self._module_name,
|
|
442
|
+
package=self._package,
|
|
443
|
+
extra=self._extra,
|
|
444
|
+
feature=self._feature,
|
|
445
|
+
)
|
|
446
|
+
return self._module
|
|
447
|
+
|
|
448
|
+
def __getattr__(self, name: str) -> Any:
|
|
449
|
+
"""Delegate attribute access to the loaded module."""
|
|
450
|
+
module = self._load()
|
|
451
|
+
return getattr(module, name)
|
|
452
|
+
|
|
453
|
+
def __dir__(self) -> list[str]:
|
|
454
|
+
"""Return module attributes for tab completion."""
|
|
455
|
+
try:
|
|
456
|
+
module = self._load()
|
|
457
|
+
return dir(module)
|
|
458
|
+
except DependencyError:
|
|
459
|
+
return []
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# =============================================================================
|
|
463
|
+
# Convenience Functions
|
|
464
|
+
# =============================================================================
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def check_dependencies(*packages: str, extra: Optional[str] = None) -> None:
|
|
468
|
+
"""
|
|
469
|
+
Check that all required packages are available.
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
*packages : str
|
|
474
|
+
Package names to check.
|
|
475
|
+
extra : str, optional
|
|
476
|
+
pytcl extra for installation hint.
|
|
477
|
+
|
|
478
|
+
Raises
|
|
479
|
+
------
|
|
480
|
+
DependencyError
|
|
481
|
+
If any package is not available.
|
|
482
|
+
|
|
483
|
+
Examples
|
|
484
|
+
--------
|
|
485
|
+
>>> from pytcl.core.optional_deps import check_dependencies
|
|
486
|
+
>>> check_dependencies("plotly", extra="visualization")
|
|
487
|
+
>>> # Raises DependencyError if plotly is not installed
|
|
488
|
+
"""
|
|
489
|
+
missing = [pkg for pkg in packages if not is_available(pkg)]
|
|
490
|
+
|
|
491
|
+
if missing:
|
|
492
|
+
feature = _get_feature_name(missing[0])
|
|
493
|
+
install_cmd = _get_install_command(missing[0], extra)
|
|
494
|
+
|
|
495
|
+
if len(missing) == 1:
|
|
496
|
+
msg = f"{missing[0]} is required. Install with: {install_cmd}"
|
|
497
|
+
else:
|
|
498
|
+
msg = f"{', '.join(missing)} are required. Install with: {install_cmd}"
|
|
499
|
+
|
|
500
|
+
raise DependencyError(
|
|
501
|
+
msg,
|
|
502
|
+
package=missing[0],
|
|
503
|
+
feature=feature,
|
|
504
|
+
install_command=install_cmd,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
__all__ = [
|
|
509
|
+
# Core functions
|
|
510
|
+
"is_available",
|
|
511
|
+
"import_optional",
|
|
512
|
+
"requires",
|
|
513
|
+
"check_dependencies",
|
|
514
|
+
# Lazy loading
|
|
515
|
+
"LazyModule",
|
|
516
|
+
# Configuration
|
|
517
|
+
"PACKAGE_EXTRAS",
|
|
518
|
+
"PACKAGE_FEATURES",
|
|
519
|
+
# Availability flags (backward compatibility)
|
|
520
|
+
"HAS_PLOTLY",
|
|
521
|
+
"HAS_PYWT",
|
|
522
|
+
"PYWT_AVAILABLE",
|
|
523
|
+
"HAS_JPLEPHEM",
|
|
524
|
+
"HAS_ASTROPY",
|
|
525
|
+
"HAS_PYPROJ",
|
|
526
|
+
"HAS_CVXPY",
|
|
527
|
+
"HAS_NETCDF4",
|
|
528
|
+
# Internal (for testing)
|
|
529
|
+
"_clear_cache",
|
|
530
|
+
"_flags",
|
|
531
|
+
]
|
pytcl/core/validation.py
CHANGED
|
@@ -14,16 +14,14 @@ from typing import Any, Callable, Literal, Sequence, TypeVar
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from numpy.typing import ArrayLike, NDArray
|
|
16
16
|
|
|
17
|
+
# Import ValidationError from exceptions module for consistency
|
|
18
|
+
# Re-export for backward compatibility
|
|
19
|
+
from pytcl.core.exceptions import ValidationError
|
|
20
|
+
|
|
17
21
|
# Type variable for generic function signatures
|
|
18
22
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class ValidationError(ValueError):
|
|
22
|
-
"""Exception raised when input validation fails."""
|
|
23
|
-
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
25
|
def validate_array(
|
|
28
26
|
arr: ArrayLike,
|
|
29
27
|
name: str = "array",
|