Qubx 0.6.68__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.71__cp312-cp312-manylinux_2_39_x86_64.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.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- qubx/__init__.py +1 -1
- qubx/cli/commands.py +4 -0
- qubx/cli/release.py +333 -25
- qubx/connectors/ccxt/adapters/__init__.py +7 -0
- qubx/connectors/ccxt/adapters/polling_adapter.py +439 -0
- qubx/connectors/ccxt/exchanges/__init__.py +7 -0
- qubx/connectors/ccxt/exchanges/binance/exchange.py +148 -1
- qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +1 -0
- qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +161 -0
- qubx/connectors/ccxt/handlers/factory.py +1 -0
- qubx/connectors/ccxt/handlers/funding_rate.py +147 -21
- qubx/connectors/ccxt/handlers/ohlc.py +210 -66
- qubx/connectors/ccxt/handlers/open_interest.py +1 -3
- qubx/connectors/ccxt/handlers/orderbook.py +43 -36
- qubx/connectors/ccxt/handlers/quote.py +1 -1
- qubx/connectors/ccxt/reader.py +325 -2
- qubx/connectors/ccxt/subscription_config.py +49 -7
- qubx/connectors/ccxt/subscription_orchestrator.py +120 -7
- qubx/connectors/ccxt/warmup_service.py +27 -24
- qubx/core/basics.py +7 -5
- qubx/core/helpers.py +6 -6
- qubx/core/lookups.py +1 -1
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/data/composite.py +252 -9
- qubx/data/readers.py +2 -2
- qubx/gathering/simplest.py +1 -1
- qubx/pandaz/ta.py +105 -0
- qubx/resources/crypto-fees.ini +8 -1
- qubx/resources/instruments/hyperliquid-spot.json +4204 -0
- qubx/resources/instruments/hyperliquid.f-perpetual.json +4424 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/marketdata/ccxt.py +6 -1
- qubx/utils/runner/_jupyter_runner.pyt +1 -1
- qubx/utils/runner/configs.py +41 -1
- qubx/utils/runner/factory.py +35 -0
- qubx/utils/runner/runner.py +22 -7
- {qubx-0.6.68.dist-info → qubx-0.6.71.dist-info}/METADATA +1 -1
- {qubx-0.6.68.dist-info → qubx-0.6.71.dist-info}/RECORD +42 -38
- qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -1
- qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -1
- {qubx-0.6.68.dist-info → qubx-0.6.71.dist-info}/LICENSE +0 -0
- {qubx-0.6.68.dist-info → qubx-0.6.71.dist-info}/WHEEL +0 -0
- {qubx-0.6.68.dist-info → qubx-0.6.71.dist-info}/entry_points.txt +0 -0
qubx/__init__.py
CHANGED
|
@@ -186,7 +186,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
186
186
|
return
|
|
187
187
|
|
|
188
188
|
ipy = get_ipython()
|
|
189
|
-
for a in [x for x in re.split(r"[\
|
|
189
|
+
for a in [x for x in re.split(r"[\s,;]", line.strip()) if x]:
|
|
190
190
|
ipy.push({a: self._get_manager().Value(None, None)})
|
|
191
191
|
|
|
192
192
|
# code to run
|
qubx/cli/commands.py
CHANGED
|
@@ -32,6 +32,10 @@ def main(debug: bool, debug_port: int, log_level: str):
|
|
|
32
32
|
"""
|
|
33
33
|
Qubx CLI.
|
|
34
34
|
"""
|
|
35
|
+
# Suppress syntax warnings from AST parsing during import resolution
|
|
36
|
+
import warnings
|
|
37
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
|
38
|
+
|
|
35
39
|
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
|
|
36
40
|
log_level = log_level.upper() if not debug else "DEBUG"
|
|
37
41
|
|
qubx/cli/release.py
CHANGED
|
@@ -37,6 +37,16 @@ Import = namedtuple("Import", ["module", "name", "alias"])
|
|
|
37
37
|
DEFAULT_CFG_NAME = "config.yml"
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class ImportResolutionError(Exception):
|
|
41
|
+
"""Raised when import resolution fails."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DependencyResolutionError(Exception):
|
|
46
|
+
"""Raised when dependency file resolution fails."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
@dataclass
|
|
41
51
|
class ReleaseInfo:
|
|
42
52
|
tag: str
|
|
@@ -53,23 +63,139 @@ class StrategyInfo:
|
|
|
53
63
|
config: StrategyConfig
|
|
54
64
|
|
|
55
65
|
|
|
56
|
-
def
|
|
66
|
+
def resolve_relative_import(relative_module: str, file_path: str, project_root: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Resolve a relative import to an absolute module path.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
relative_module: The relative module string (e.g., "..utils", ".helper")
|
|
72
|
+
file_path: Absolute path to the file containing the relative import
|
|
73
|
+
project_root: Root directory of the project
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Absolute module path string
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ImportResolutionError: If the relative import cannot be resolved
|
|
80
|
+
"""
|
|
81
|
+
# Get the relative path from project root to the file
|
|
82
|
+
rel_file_path = os.path.relpath(file_path, project_root)
|
|
83
|
+
|
|
84
|
+
# Get the directory containing the file (remove filename)
|
|
85
|
+
file_dir = os.path.dirname(rel_file_path)
|
|
86
|
+
|
|
87
|
+
# Convert file directory path to module path
|
|
88
|
+
if file_dir:
|
|
89
|
+
current_module_parts = file_dir.replace(os.sep, ".").split(".")
|
|
90
|
+
else:
|
|
91
|
+
current_module_parts = []
|
|
92
|
+
|
|
93
|
+
# Parse the relative import
|
|
94
|
+
level = 0
|
|
95
|
+
module_name = relative_module
|
|
96
|
+
|
|
97
|
+
# Count leading dots to determine level
|
|
98
|
+
while module_name.startswith("."):
|
|
99
|
+
level += 1
|
|
100
|
+
module_name = module_name[1:]
|
|
101
|
+
|
|
102
|
+
# Calculate the target module parts
|
|
103
|
+
if level == 0:
|
|
104
|
+
# Not actually a relative import
|
|
105
|
+
return module_name
|
|
106
|
+
|
|
107
|
+
# For relative imports, we need to go up from the current package
|
|
108
|
+
# level-1 because level=1 means "same package", level=2 means "parent package"
|
|
109
|
+
if level == 1:
|
|
110
|
+
# from .module -> current package + module
|
|
111
|
+
parent_parts = current_module_parts
|
|
112
|
+
else:
|
|
113
|
+
# from ..module -> parent package + module
|
|
114
|
+
levels_up = level - 1
|
|
115
|
+
if levels_up > len(current_module_parts):
|
|
116
|
+
raise ImportResolutionError(
|
|
117
|
+
f"Relative import '{relative_module}' goes beyond project root in {file_path}"
|
|
118
|
+
)
|
|
119
|
+
parent_parts = current_module_parts[:-levels_up] if levels_up > 0 else current_module_parts
|
|
120
|
+
|
|
121
|
+
# Combine parent with the remaining module name
|
|
122
|
+
if module_name:
|
|
123
|
+
resolved_parts = parent_parts + module_name.split(".")
|
|
124
|
+
else:
|
|
125
|
+
resolved_parts = parent_parts
|
|
126
|
+
|
|
127
|
+
return ".".join(resolved_parts) if resolved_parts else ""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_imports(
|
|
131
|
+
path: str,
|
|
132
|
+
what_to_look: list[str] = ["xincubator"],
|
|
133
|
+
project_root: str | None = None
|
|
134
|
+
) -> Generator[Import, None, None]:
|
|
57
135
|
"""
|
|
58
136
|
Get imports from the given file.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
path: Path to Python file to analyze
|
|
140
|
+
what_to_look: List of module prefixes to filter for (empty list = no filter)
|
|
141
|
+
project_root: Root directory for resolving relative imports (optional)
|
|
142
|
+
|
|
143
|
+
Yields:
|
|
144
|
+
Import namedtuples for each matching import statement
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
SyntaxError: If the Python file has syntax errors
|
|
148
|
+
FileNotFoundError: If the file doesn't exist
|
|
149
|
+
ImportResolutionError: If relative imports cannot be resolved
|
|
59
150
|
"""
|
|
60
151
|
with open(path) as fh:
|
|
61
152
|
root = ast.parse(fh.read(), path)
|
|
62
153
|
|
|
63
154
|
for node in ast.iter_child_nodes(root):
|
|
64
155
|
if isinstance(node, ast.Import):
|
|
65
|
-
module
|
|
156
|
+
# Handle direct imports like: import module, import module.submodule
|
|
157
|
+
for n in node.names:
|
|
158
|
+
module_parts = n.name.split(".")
|
|
159
|
+
# Apply filter if provided
|
|
160
|
+
if not what_to_look or module_parts[0] in what_to_look:
|
|
161
|
+
yield Import(module_parts, module_parts[-1:], n.asname)
|
|
162
|
+
|
|
66
163
|
elif isinstance(node, ast.ImportFrom):
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
164
|
+
# Handle from imports like: from module import name
|
|
165
|
+
level = getattr(node, 'level', 0)
|
|
166
|
+
|
|
167
|
+
if level > 0:
|
|
168
|
+
# This is a relative import (has dots)
|
|
169
|
+
if project_root is None:
|
|
170
|
+
# Skip relative imports if no project root provided
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Build the relative module string
|
|
174
|
+
relative_module = "." * level
|
|
175
|
+
if node.module:
|
|
176
|
+
relative_module += node.module
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Resolve relative import to absolute module path
|
|
180
|
+
resolved_module = resolve_relative_import(relative_module, path, project_root)
|
|
181
|
+
if resolved_module:
|
|
182
|
+
module_parts = resolved_module.split(".")
|
|
183
|
+
# Apply filter if provided
|
|
184
|
+
if not what_to_look or (module_parts and module_parts[0] in what_to_look):
|
|
185
|
+
for n in node.names:
|
|
186
|
+
yield Import(module_parts, n.name.split("."), n.asname)
|
|
187
|
+
except ImportResolutionError as e:
|
|
188
|
+
# Log the error but don't fail completely
|
|
189
|
+
logger.warning(f"Failed to resolve relative import: {e}")
|
|
190
|
+
continue
|
|
191
|
+
else:
|
|
192
|
+
# Regular from import (no dots)
|
|
193
|
+
if node.module:
|
|
194
|
+
module_parts = node.module.split(".")
|
|
195
|
+
# Apply filter if provided
|
|
196
|
+
if not what_to_look or module_parts[0] in what_to_look:
|
|
197
|
+
for n in node.names:
|
|
198
|
+
yield Import(module_parts, n.name.split("."), n.asname)
|
|
73
199
|
|
|
74
200
|
|
|
75
201
|
def ls_strats(path: str) -> None:
|
|
@@ -409,6 +535,12 @@ def _copy_strategy_file(strategy_path: str, pyproject_root: str, release_dir: st
|
|
|
409
535
|
def _try_copy_file(src_file: str, dest_dir: str, pyproject_root: str) -> None:
|
|
410
536
|
"""Try to copy the file to the release directory."""
|
|
411
537
|
if os.path.exists(src_file):
|
|
538
|
+
# Skip unwanted files
|
|
539
|
+
file_name = os.path.basename(src_file)
|
|
540
|
+
if file_name.endswith('.pyc') or file_name.startswith('.'):
|
|
541
|
+
logger.debug(f"Skipping unwanted file: {src_file}")
|
|
542
|
+
return
|
|
543
|
+
|
|
412
544
|
# Get the relative path from pyproject_root
|
|
413
545
|
_rel_import_path = os.path.relpath(src_file, pyproject_root)
|
|
414
546
|
_dest_import_path = os.path.join(dest_dir, _rel_import_path)
|
|
@@ -421,35 +553,168 @@ def _try_copy_file(src_file: str, dest_dir: str, pyproject_root: str) -> None:
|
|
|
421
553
|
shutil.copy2(src_file, _dest_import_path)
|
|
422
554
|
|
|
423
555
|
|
|
556
|
+
def _copy_package_directory(src_package_dir: str, dest_dir: str, pyproject_root: str) -> None:
|
|
557
|
+
"""
|
|
558
|
+
Copy an entire package directory recursively to the release directory.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
src_package_dir: Source package directory path
|
|
562
|
+
dest_dir: Destination release directory
|
|
563
|
+
pyproject_root: Project root for calculating relative paths
|
|
564
|
+
"""
|
|
565
|
+
if not os.path.exists(src_package_dir):
|
|
566
|
+
logger.warning(f"Package directory not found: {src_package_dir}")
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
# Get the relative path from pyproject_root
|
|
570
|
+
rel_package_path = os.path.relpath(src_package_dir, pyproject_root)
|
|
571
|
+
dest_package_path = os.path.join(dest_dir, rel_package_path)
|
|
572
|
+
|
|
573
|
+
# Create destination directory structure
|
|
574
|
+
os.makedirs(dest_package_path, exist_ok=True)
|
|
575
|
+
|
|
576
|
+
# Copy all files in the package directory recursively
|
|
577
|
+
for root, dirs, files in os.walk(src_package_dir):
|
|
578
|
+
# Filter out unwanted directories (modify dirs in-place to skip them)
|
|
579
|
+
dirs[:] = [d for d in dirs if not d.startswith('__pycache__') and not d.startswith('.')]
|
|
580
|
+
|
|
581
|
+
# Calculate relative path within the package
|
|
582
|
+
rel_root = os.path.relpath(root, src_package_dir)
|
|
583
|
+
|
|
584
|
+
# Create subdirectories in destination
|
|
585
|
+
if rel_root != ".":
|
|
586
|
+
dest_subdir = os.path.join(dest_package_path, rel_root)
|
|
587
|
+
os.makedirs(dest_subdir, exist_ok=True)
|
|
588
|
+
else:
|
|
589
|
+
dest_subdir = dest_package_path
|
|
590
|
+
|
|
591
|
+
# Copy all files (filter out unwanted files)
|
|
592
|
+
for file_name in files:
|
|
593
|
+
# Skip unwanted files
|
|
594
|
+
if file_name.endswith('.pyc') or file_name.startswith('.'):
|
|
595
|
+
continue
|
|
596
|
+
|
|
597
|
+
src_file = os.path.join(root, file_name)
|
|
598
|
+
dest_file = os.path.join(dest_subdir, file_name)
|
|
599
|
+
|
|
600
|
+
logger.debug(f"Copying package file from {src_file} to {dest_file}")
|
|
601
|
+
shutil.copy2(src_file, dest_file)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _validate_dependencies(imports: list[Import], src_root: str, src_dir: str) -> tuple[list[Import], list[str]]:
|
|
605
|
+
"""
|
|
606
|
+
Validate that all discovered dependencies can be resolved to actual files.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
imports: List of Import objects to validate
|
|
610
|
+
src_root: Root directory containing the source packages
|
|
611
|
+
src_dir: Name of the source directory/package
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Tuple of (valid_imports, missing_dependencies)
|
|
615
|
+
"""
|
|
616
|
+
valid_imports = []
|
|
617
|
+
missing_dependencies = []
|
|
618
|
+
|
|
619
|
+
for imp in imports:
|
|
620
|
+
# Construct expected path for this import
|
|
621
|
+
module_path_parts = [s for s in imp.module if s != src_dir]
|
|
622
|
+
base_path = os.path.join(src_root, *module_path_parts)
|
|
623
|
+
|
|
624
|
+
# Check if the import can be resolved to an actual file or package
|
|
625
|
+
found = False
|
|
626
|
+
|
|
627
|
+
# Try different file extensions and package structures
|
|
628
|
+
possible_paths = [
|
|
629
|
+
base_path + ".py",
|
|
630
|
+
base_path + ".pyx",
|
|
631
|
+
base_path + ".pyi",
|
|
632
|
+
base_path + ".pxd",
|
|
633
|
+
os.path.join(base_path, "__init__.py")
|
|
634
|
+
]
|
|
635
|
+
|
|
636
|
+
for path in possible_paths:
|
|
637
|
+
if os.path.exists(path):
|
|
638
|
+
valid_imports.append(imp)
|
|
639
|
+
found = True
|
|
640
|
+
break
|
|
641
|
+
|
|
642
|
+
if not found:
|
|
643
|
+
missing_dependencies.append(f"{'.'.join(imp.module)} -> searched: {', '.join(possible_paths)}")
|
|
644
|
+
|
|
645
|
+
return valid_imports, missing_dependencies
|
|
646
|
+
|
|
647
|
+
|
|
424
648
|
def _copy_dependencies(strategy_path: str, pyproject_root: str, release_dir: str) -> None:
|
|
425
|
-
"""
|
|
649
|
+
"""
|
|
650
|
+
Copy all dependencies required by the strategy with validation.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
strategy_path: Path to the main strategy file
|
|
654
|
+
pyproject_root: Root directory of the project
|
|
655
|
+
release_dir: Destination directory for the release
|
|
656
|
+
|
|
657
|
+
Raises:
|
|
658
|
+
DependencyResolutionError: If critical dependencies cannot be resolved
|
|
659
|
+
"""
|
|
426
660
|
_src_dir = os.path.basename(pyproject_root)
|
|
427
|
-
|
|
661
|
+
|
|
428
662
|
# find inside of the pyproject_root a folder with the same name as the _src_dir
|
|
429
663
|
# for instance it could be like macd_crossover/src/macd_crossover
|
|
430
664
|
# or macd_crossover/macd_crossover
|
|
431
665
|
# and assign this folder to _src_root
|
|
432
666
|
_src_root = None
|
|
667
|
+
potential_roots = []
|
|
433
668
|
for root, dirs, files in os.walk(pyproject_root):
|
|
434
669
|
if _src_dir in dirs:
|
|
435
|
-
|
|
670
|
+
potential_root = os.path.join(root, _src_dir)
|
|
671
|
+
potential_roots.append(potential_root)
|
|
672
|
+
|
|
673
|
+
# Prefer source directories (src/package_name) over root directories (package_name)
|
|
674
|
+
# This handles cases where both /project/package_name AND /project/src/package_name exist
|
|
675
|
+
for root in potential_roots:
|
|
676
|
+
if os.path.sep + "src" + os.path.sep in root:
|
|
677
|
+
_src_root = root
|
|
436
678
|
break
|
|
679
|
+
|
|
680
|
+
# If no src-based root found, use the first one (for simpler structures)
|
|
681
|
+
if _src_root is None and potential_roots:
|
|
682
|
+
_src_root = potential_roots[0]
|
|
437
683
|
|
|
438
684
|
if _src_root is None:
|
|
439
|
-
raise
|
|
440
|
-
|
|
441
|
-
|
|
685
|
+
raise DependencyResolutionError(f"Could not find the source root for {_src_dir} in {pyproject_root}")
|
|
686
|
+
|
|
687
|
+
# Now call _get_imports with the correct source root directory
|
|
688
|
+
_imports = _get_imports(strategy_path, _src_root, [_src_dir])
|
|
689
|
+
|
|
690
|
+
# Validate all dependencies before copying
|
|
691
|
+
valid_imports, missing_dependencies = _validate_dependencies(_imports, _src_root, _src_dir)
|
|
692
|
+
|
|
693
|
+
if missing_dependencies:
|
|
694
|
+
logger.warning(f"Found {len(missing_dependencies)} missing dependencies:")
|
|
695
|
+
for missing in missing_dependencies:
|
|
696
|
+
logger.warning(f" - {missing}")
|
|
697
|
+
logger.warning("Release package may be incomplete. Consider fixing missing dependencies.")
|
|
698
|
+
|
|
699
|
+
logger.info(f"Copying {len(valid_imports)} validated dependencies...")
|
|
700
|
+
|
|
701
|
+
# Copy only the valid dependencies
|
|
702
|
+
for _imp in valid_imports:
|
|
442
703
|
# Construct source path
|
|
443
704
|
_base = os.path.join(_src_root, *[s for s in _imp.module if s != _src_dir])
|
|
444
705
|
|
|
445
706
|
# - try to copy all available files for satisfying the import
|
|
446
707
|
if os.path.isdir(_base):
|
|
447
|
-
|
|
708
|
+
# This is a package directory - copy all files recursively
|
|
709
|
+
_copy_package_directory(_base, release_dir, pyproject_root)
|
|
448
710
|
else:
|
|
711
|
+
# This is a single module - copy all variants
|
|
449
712
|
_try_copy_file(_base + ".py", release_dir, pyproject_root)
|
|
450
713
|
_try_copy_file(_base + ".pyx", release_dir, pyproject_root)
|
|
451
714
|
_try_copy_file(_base + ".pyi", release_dir, pyproject_root)
|
|
452
715
|
_try_copy_file(_base + ".pxd", release_dir, pyproject_root)
|
|
716
|
+
|
|
717
|
+
logger.info(f"Successfully copied {len(valid_imports)} dependencies to release package")
|
|
453
718
|
|
|
454
719
|
|
|
455
720
|
def _create_metadata(stg_name: str, git_info: ReleaseInfo, release_dir: str) -> None:
|
|
@@ -656,19 +921,62 @@ def _create_zip_archive(output_dir: str, release_dir: str, tag: str) -> None:
|
|
|
656
921
|
|
|
657
922
|
|
|
658
923
|
def _get_imports(file_name: str, current_directory: str, what_to_look: list[str]) -> list[Import]:
|
|
659
|
-
|
|
924
|
+
"""
|
|
925
|
+
Recursively get all imports from a file and its dependencies.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
file_name: Path to the Python file to analyze
|
|
929
|
+
current_directory: Root directory for resolving imports
|
|
930
|
+
what_to_look: List of module prefixes to filter for
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
List of Import objects for all discovered dependencies
|
|
934
|
+
|
|
935
|
+
Raises:
|
|
936
|
+
DependencyResolutionError: If a required dependency cannot be found or processed
|
|
937
|
+
"""
|
|
938
|
+
try:
|
|
939
|
+
imports = list(get_imports(file_name, what_to_look, project_root=current_directory))
|
|
940
|
+
except (SyntaxError, FileNotFoundError) as e:
|
|
941
|
+
raise DependencyResolutionError(f"Failed to parse imports from {file_name}: {e}")
|
|
942
|
+
|
|
660
943
|
current_dirname = os.path.basename(current_directory)
|
|
944
|
+
missing_dependencies = []
|
|
945
|
+
|
|
661
946
|
for i in imports:
|
|
662
947
|
try:
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
948
|
+
# Build path to the imported module
|
|
949
|
+
module_path_parts = [s for s in i.module if s != current_dirname]
|
|
950
|
+
base = os.path.join(current_directory, *module_path_parts)
|
|
951
|
+
|
|
952
|
+
# Try to find the dependency file
|
|
953
|
+
dependency_file = None
|
|
954
|
+
if os.path.exists(base + ".py"):
|
|
955
|
+
dependency_file = base + ".py"
|
|
956
|
+
elif os.path.exists(os.path.join(base, "__init__.py")):
|
|
957
|
+
dependency_file = os.path.join(base, "__init__.py")
|
|
958
|
+
|
|
959
|
+
if dependency_file:
|
|
960
|
+
# Recursively process the dependency
|
|
961
|
+
try:
|
|
962
|
+
imports.extend(_get_imports(dependency_file, current_directory, what_to_look))
|
|
963
|
+
except DependencyResolutionError as e:
|
|
964
|
+
# Log nested dependency errors but continue processing
|
|
965
|
+
logger.warning(f"Failed to resolve nested dependency: {e}")
|
|
966
|
+
else:
|
|
967
|
+
# Track missing dependencies
|
|
968
|
+
missing_dependencies.append(f"{'.'.join(i.module)} (searched: {base}.py, {base}/__init__.py)")
|
|
969
|
+
|
|
970
|
+
except Exception as e:
|
|
971
|
+
# Convert unexpected errors to DependencyResolutionError
|
|
972
|
+
raise DependencyResolutionError(f"Unexpected error processing import {'.'.join(i.module)}: {e}")
|
|
973
|
+
|
|
974
|
+
# Warn about missing dependencies but don't fail completely
|
|
975
|
+
if missing_dependencies:
|
|
976
|
+
logger.warning(f"Could not resolve {len(missing_dependencies)} dependencies from {file_name}:")
|
|
977
|
+
for dep in missing_dependencies:
|
|
978
|
+
logger.warning(f" - {dep}")
|
|
979
|
+
|
|
672
980
|
return imports
|
|
673
981
|
|
|
674
982
|
|