dagflow-cli-plugin 0.1.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.
- dagflow_cli/__init__.py +1 -0
- dagflow_cli/commands.py +423 -0
- dagflow_cli/launch.py +118 -0
- dagflow_cli/manager.py +283 -0
- dagflow_cli/pex.py +523 -0
- dagflow_cli/plugin_spec.py +19 -0
- dagflow_cli/runtime-platforms.json +1221 -0
- dagflow_cli/workspace.py +215 -0
- dagflow_cli_plugin-0.1.0.dist-info/METADATA +30 -0
- dagflow_cli_plugin-0.1.0.dist-info/RECORD +13 -0
- dagflow_cli_plugin-0.1.0.dist-info/WHEEL +5 -0
- dagflow_cli_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- dagflow_cli_plugin-0.1.0.dist-info/top_level.txt +1 -0
dagflow_cli/pex.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import zipfile
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import ContextManager, Iterator, Sequence
|
|
17
|
+
|
|
18
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
19
|
+
from packaging.utils import InvalidWheelFilename, canonicalize_name, parse_wheel_filename
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import tomllib
|
|
23
|
+
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
|
|
24
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class PexArtifacts:
|
|
29
|
+
source_pex: Path
|
|
30
|
+
deps_pex: Path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_PYTHON_VERSION = "3.11"
|
|
34
|
+
SUPPORTED_PYTHON_VERSIONS = {"3.11", "3.12"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class PexBuildConfig:
|
|
39
|
+
project_path: Path
|
|
40
|
+
output_dir: Path
|
|
41
|
+
python_version: str = DEFAULT_PYTHON_VERSION
|
|
42
|
+
python: str = sys.executable
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class DependencyBuildInputs:
|
|
47
|
+
requirements: tuple[str, ...]
|
|
48
|
+
direct_requirements: tuple[str, ...]
|
|
49
|
+
local_packages: tuple[Path, ...]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class RequirementEntry:
|
|
54
|
+
value: str
|
|
55
|
+
relative_to: Path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
SOURCE_IGNORED_PATTERNS = (
|
|
59
|
+
".git",
|
|
60
|
+
".venv",
|
|
61
|
+
".pytest_cache",
|
|
62
|
+
".tox",
|
|
63
|
+
".nox",
|
|
64
|
+
".mypy_cache",
|
|
65
|
+
".cache",
|
|
66
|
+
"__pycache__",
|
|
67
|
+
"*.pyc",
|
|
68
|
+
"*.egg-info",
|
|
69
|
+
".dagflow-*",
|
|
70
|
+
"build",
|
|
71
|
+
"dist",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_pex_artifacts(config: PexBuildConfig) -> PexArtifacts:
|
|
76
|
+
project_path = config.project_path.expanduser().resolve()
|
|
77
|
+
if not project_path.exists():
|
|
78
|
+
raise ValueError(f"project path does not exist: {project_path}")
|
|
79
|
+
target_python = _target_python(config.python_version)
|
|
80
|
+
|
|
81
|
+
output_dir = config.output_dir.expanduser().resolve()
|
|
82
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
source_pex = output_dir / "source.pex"
|
|
84
|
+
deps_pex = output_dir / "deps.pex"
|
|
85
|
+
|
|
86
|
+
inputs = _collect_dependency_inputs(project_path, target_python)
|
|
87
|
+
_validate_runtime_dependencies(inputs.direct_requirements)
|
|
88
|
+
with _requirements_file(inputs.requirements, project_path) as requirements:
|
|
89
|
+
with _staged_source_directories(
|
|
90
|
+
project_path,
|
|
91
|
+
inputs.local_packages,
|
|
92
|
+
target_python,
|
|
93
|
+
output_dir,
|
|
94
|
+
) as source_directories:
|
|
95
|
+
_run_pex(
|
|
96
|
+
_source_pex_command(
|
|
97
|
+
config.python,
|
|
98
|
+
target_python,
|
|
99
|
+
source_directories,
|
|
100
|
+
source_pex,
|
|
101
|
+
),
|
|
102
|
+
cwd=project_path,
|
|
103
|
+
)
|
|
104
|
+
with _complete_platform_file(config.python_version) as complete_platform:
|
|
105
|
+
_run_pex(
|
|
106
|
+
_deps_pex_command(config.python, complete_platform, requirements, deps_pex),
|
|
107
|
+
cwd=project_path,
|
|
108
|
+
)
|
|
109
|
+
_validate_deps_pex(deps_pex)
|
|
110
|
+
|
|
111
|
+
return PexArtifacts(source_pex=source_pex, deps_pex=deps_pex)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def default_output_dir(location_name: str, version: str) -> Path:
|
|
115
|
+
return Path("dist") / "dagflow" / location_name / version
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def default_version() -> str:
|
|
119
|
+
try:
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
["git", "rev-parse", "--short=12", "HEAD"],
|
|
122
|
+
check=True,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
)
|
|
126
|
+
except (OSError, subprocess.CalledProcessError):
|
|
127
|
+
from datetime import datetime, timezone
|
|
128
|
+
|
|
129
|
+
return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
|
130
|
+
return result.stdout.strip()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _target_python(python_version: str) -> str:
|
|
134
|
+
if python_version not in SUPPORTED_PYTHON_VERSIONS:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"unsupported python version {python_version}; supported versions are "
|
|
137
|
+
+ ", ".join(sorted(SUPPORTED_PYTHON_VERSIONS))
|
|
138
|
+
)
|
|
139
|
+
current_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
140
|
+
if python_version == current_version:
|
|
141
|
+
return sys.executable
|
|
142
|
+
executable = f"python{python_version}"
|
|
143
|
+
resolved = shutil.which(executable)
|
|
144
|
+
if resolved is None:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"{executable} was not found on PATH; install Python {python_version} "
|
|
147
|
+
f"or choose a runtime version available in this environment"
|
|
148
|
+
)
|
|
149
|
+
return resolved
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@contextmanager
|
|
153
|
+
def _requirements_file(requirements: Sequence[str], project_path: Path) -> Iterator[Path]:
|
|
154
|
+
generated = tempfile.NamedTemporaryFile(
|
|
155
|
+
"w",
|
|
156
|
+
encoding="utf-8",
|
|
157
|
+
suffix="-requirements.txt",
|
|
158
|
+
prefix=".dagflow-",
|
|
159
|
+
dir=project_path,
|
|
160
|
+
delete=False,
|
|
161
|
+
)
|
|
162
|
+
path = Path(generated.name)
|
|
163
|
+
try:
|
|
164
|
+
generated.write("\n".join(requirements))
|
|
165
|
+
generated.write("\n")
|
|
166
|
+
generated.close()
|
|
167
|
+
yield path
|
|
168
|
+
finally:
|
|
169
|
+
generated.close()
|
|
170
|
+
path.unlink(missing_ok=True)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@contextmanager
|
|
174
|
+
def _temporary_json_file(value: object) -> Iterator[Path]:
|
|
175
|
+
generated = tempfile.NamedTemporaryFile(
|
|
176
|
+
"w", encoding="utf-8", suffix="-complete-platform.json", delete=False
|
|
177
|
+
)
|
|
178
|
+
path = Path(generated.name)
|
|
179
|
+
try:
|
|
180
|
+
json.dump(value, generated)
|
|
181
|
+
generated.close()
|
|
182
|
+
yield path
|
|
183
|
+
finally:
|
|
184
|
+
generated.close()
|
|
185
|
+
path.unlink(missing_ok=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _runtime_complete_platform(python_version: str) -> dict[str, object]:
|
|
189
|
+
manifest = json.loads(
|
|
190
|
+
resources.files("dagflow_cli")
|
|
191
|
+
.joinpath("runtime-platforms.json")
|
|
192
|
+
.read_text(encoding="utf-8")
|
|
193
|
+
)[python_version]
|
|
194
|
+
return manifest["complete_platform"]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _complete_platform_file(python_version: str) -> ContextManager[Path]:
|
|
198
|
+
return _temporary_json_file(_runtime_complete_platform(python_version))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _deps_pex_command(python: str, complete_platform: Path, requirements: Path, output: Path) -> list[str]:
|
|
202
|
+
return [
|
|
203
|
+
python,
|
|
204
|
+
"-m",
|
|
205
|
+
"pex",
|
|
206
|
+
"--complete-platform",
|
|
207
|
+
str(complete_platform),
|
|
208
|
+
"-r",
|
|
209
|
+
str(requirements),
|
|
210
|
+
"--pip-version",
|
|
211
|
+
"latest-compatible",
|
|
212
|
+
"--resolver-version",
|
|
213
|
+
"pip-2020-resolver",
|
|
214
|
+
"--inherit-path=false",
|
|
215
|
+
"--no-strip-pex-env",
|
|
216
|
+
"-o",
|
|
217
|
+
str(output),
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _source_pex_command(
|
|
222
|
+
python: str,
|
|
223
|
+
target_python: str,
|
|
224
|
+
source_directories: Sequence[Path],
|
|
225
|
+
output: Path,
|
|
226
|
+
) -> list[str]:
|
|
227
|
+
source_args = [arg for path in source_directories for arg in ("-D", str(path))]
|
|
228
|
+
return [
|
|
229
|
+
python,
|
|
230
|
+
"-m",
|
|
231
|
+
"pex",
|
|
232
|
+
"--python",
|
|
233
|
+
target_python,
|
|
234
|
+
*source_args,
|
|
235
|
+
"--inherit-path=false",
|
|
236
|
+
"--no-strip-pex-env",
|
|
237
|
+
"-o",
|
|
238
|
+
str(output),
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _run_pex(command: Sequence[str], cwd: Path) -> None:
|
|
243
|
+
env = {**os.environ, "PEX_ROOT": os.environ.get("PEX_ROOT", str(Path.home() / ".pex"))}
|
|
244
|
+
subprocess.run(command, cwd=cwd, env=env, check=True)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@contextmanager
|
|
248
|
+
def _staged_source_directories(
|
|
249
|
+
project_path: Path,
|
|
250
|
+
local_packages: Sequence[Path],
|
|
251
|
+
python: str,
|
|
252
|
+
output_dir: Path | None = None,
|
|
253
|
+
) -> Iterator[tuple[Path, ...]]:
|
|
254
|
+
with tempfile.TemporaryDirectory(prefix="dagflow-source-") as temp_dir:
|
|
255
|
+
staging_root = Path(temp_dir)
|
|
256
|
+
build_dir = staging_root / "installed"
|
|
257
|
+
source_dir = staging_root / "source"
|
|
258
|
+
build_dir.mkdir()
|
|
259
|
+
|
|
260
|
+
for local_package in (*local_packages, project_path):
|
|
261
|
+
if _is_buildable_project(local_package):
|
|
262
|
+
_install_project_source(local_package, build_dir, python)
|
|
263
|
+
|
|
264
|
+
base_ignore = shutil.ignore_patterns(*SOURCE_IGNORED_PATTERNS)
|
|
265
|
+
|
|
266
|
+
def ignore(directory: str, names: list[str]) -> set[str]:
|
|
267
|
+
ignored = set(base_ignore(directory, names))
|
|
268
|
+
if output_dir is not None:
|
|
269
|
+
for name in names:
|
|
270
|
+
if (Path(directory) / name).resolve() == output_dir.resolve():
|
|
271
|
+
ignored.add(name)
|
|
272
|
+
return ignored
|
|
273
|
+
|
|
274
|
+
shutil.copytree(
|
|
275
|
+
project_path,
|
|
276
|
+
source_dir,
|
|
277
|
+
ignore=ignore,
|
|
278
|
+
symlinks=True,
|
|
279
|
+
)
|
|
280
|
+
_remove_installed_source_collisions(source_dir, build_dir)
|
|
281
|
+
directories = [source_dir]
|
|
282
|
+
if any(build_dir.iterdir()):
|
|
283
|
+
directories.insert(0, build_dir)
|
|
284
|
+
yield tuple(directories)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _is_buildable_project(project_path: Path) -> bool:
|
|
288
|
+
return (project_path / "setup.py").exists() or (project_path / "pyproject.toml").exists()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _remove_installed_source_collisions(source_dir: Path, build_dir: Path) -> None:
|
|
292
|
+
for installed_path in build_dir.rglob("*"):
|
|
293
|
+
if not installed_path.is_file() and not installed_path.is_symlink():
|
|
294
|
+
continue
|
|
295
|
+
source_path = source_dir / installed_path.relative_to(build_dir)
|
|
296
|
+
if source_path.is_file() or source_path.is_symlink():
|
|
297
|
+
source_path.unlink()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _install_project_source(project_path: Path, build_dir: Path, python: str) -> None:
|
|
301
|
+
try:
|
|
302
|
+
subprocess.run(
|
|
303
|
+
[
|
|
304
|
+
python,
|
|
305
|
+
"-m",
|
|
306
|
+
"pip",
|
|
307
|
+
"install",
|
|
308
|
+
"--target",
|
|
309
|
+
str(build_dir),
|
|
310
|
+
"--no-deps",
|
|
311
|
+
".",
|
|
312
|
+
],
|
|
313
|
+
cwd=project_path,
|
|
314
|
+
check=True,
|
|
315
|
+
)
|
|
316
|
+
except subprocess.CalledProcessError as exc:
|
|
317
|
+
raise ValueError(f"could not build local project source at {project_path}") from exc
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _collect_dependency_inputs(project_path: Path, python: str) -> DependencyBuildInputs:
|
|
321
|
+
direct_entries = _project_dependency_entries(project_path, python)
|
|
322
|
+
pending = [(project_path, direct_entries)]
|
|
323
|
+
seen_projects: set[Path] = set()
|
|
324
|
+
local_packages: list[Path] = []
|
|
325
|
+
requirements: list[str] = []
|
|
326
|
+
|
|
327
|
+
while pending:
|
|
328
|
+
current_project, entries = pending.pop()
|
|
329
|
+
current_project = current_project.resolve()
|
|
330
|
+
if current_project in seen_projects:
|
|
331
|
+
continue
|
|
332
|
+
seen_projects.add(current_project)
|
|
333
|
+
|
|
334
|
+
for entry in entries:
|
|
335
|
+
local_path = _local_package_path(entry)
|
|
336
|
+
if local_path is None:
|
|
337
|
+
if entry.value not in requirements:
|
|
338
|
+
requirements.append(entry.value)
|
|
339
|
+
continue
|
|
340
|
+
if local_path == current_project:
|
|
341
|
+
continue
|
|
342
|
+
if not local_path.is_dir():
|
|
343
|
+
raise ValueError(
|
|
344
|
+
f"local package {local_path} referenced by {entry.value!r} is not a directory"
|
|
345
|
+
)
|
|
346
|
+
if local_path not in local_packages:
|
|
347
|
+
local_packages.append(local_path)
|
|
348
|
+
pending.append((local_path, _project_dependency_entries(local_path, python)))
|
|
349
|
+
|
|
350
|
+
return DependencyBuildInputs(
|
|
351
|
+
requirements=tuple(requirements),
|
|
352
|
+
direct_requirements=tuple(entry.value for entry in direct_entries),
|
|
353
|
+
local_packages=tuple(local_packages),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _project_dependency_entries(project_path: Path, python: str) -> list[RequirementEntry]:
|
|
358
|
+
entries: list[RequirementEntry] = []
|
|
359
|
+
requirements = project_path / "requirements.txt"
|
|
360
|
+
if requirements.exists():
|
|
361
|
+
entries.extend(_requirements_file_entries(requirements))
|
|
362
|
+
entries.extend(
|
|
363
|
+
RequirementEntry(value=value, relative_to=project_path)
|
|
364
|
+
for value in _setup_py_dependencies(project_path, python)
|
|
365
|
+
)
|
|
366
|
+
entries.extend(
|
|
367
|
+
RequirementEntry(value=value, relative_to=project_path)
|
|
368
|
+
for value in _pyproject_dependencies(project_path)
|
|
369
|
+
)
|
|
370
|
+
return entries
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _validate_runtime_dependencies(dependencies: Sequence[str]) -> None:
|
|
374
|
+
names = {_requirement_name(dependency) for dependency in dependencies}
|
|
375
|
+
names.discard(None)
|
|
376
|
+
missing = [name for name in ("dagster", "dagster-postgres") if name not in names]
|
|
377
|
+
if missing:
|
|
378
|
+
required = ", ".join(missing)
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f"deps.pex must include direct project requirement(s): {required}. "
|
|
381
|
+
"The code-location image does not provide Dagster or PostgreSQL storage classes."
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _requirements_file_entries(
|
|
386
|
+
path: Path,
|
|
387
|
+
seen: set[Path] | None = None,
|
|
388
|
+
) -> list[RequirementEntry]:
|
|
389
|
+
resolved = path.resolve()
|
|
390
|
+
visited = seen or set()
|
|
391
|
+
if resolved in visited:
|
|
392
|
+
return []
|
|
393
|
+
visited.add(resolved)
|
|
394
|
+
|
|
395
|
+
entries: list[RequirementEntry] = []
|
|
396
|
+
for raw_line in _logical_requirement_lines(resolved):
|
|
397
|
+
line = re.sub(r"(^#|\s+#).*", "", raw_line).strip()
|
|
398
|
+
if not line:
|
|
399
|
+
continue
|
|
400
|
+
parts = line.split(maxsplit=1)
|
|
401
|
+
if parts[0] in {"-r", "--requirement"} and len(parts) == 2:
|
|
402
|
+
entries.extend(_requirements_file_entries(resolved.parent / parts[1], visited))
|
|
403
|
+
elif parts[0].startswith("-r") and len(parts[0]) > 2:
|
|
404
|
+
entries.extend(_requirements_file_entries(resolved.parent / parts[0][2:], visited))
|
|
405
|
+
elif line != ".":
|
|
406
|
+
entries.append(RequirementEntry(value=line, relative_to=resolved.parent))
|
|
407
|
+
return entries
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _logical_requirement_lines(path: Path) -> list[str]:
|
|
411
|
+
lines: list[str] = []
|
|
412
|
+
current = ""
|
|
413
|
+
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
414
|
+
stripped = raw_line.rstrip()
|
|
415
|
+
if stripped.endswith("\\"):
|
|
416
|
+
current += stripped[:-1]
|
|
417
|
+
else:
|
|
418
|
+
lines.append(current + stripped)
|
|
419
|
+
current = ""
|
|
420
|
+
if current:
|
|
421
|
+
lines.append(current)
|
|
422
|
+
return lines
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _local_package_path(entry: RequirementEntry) -> Path | None:
|
|
426
|
+
value = entry.value.strip()
|
|
427
|
+
if value.startswith("-e ") or value.startswith("--editable "):
|
|
428
|
+
value = value.split(maxsplit=1)[1]
|
|
429
|
+
if not value.startswith(("./", "../", "/")):
|
|
430
|
+
return None
|
|
431
|
+
return (entry.relative_to / value).resolve()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _requirement_name(value: str) -> str | None:
|
|
435
|
+
try:
|
|
436
|
+
return canonicalize_name(Requirement(value).name)
|
|
437
|
+
except InvalidRequirement:
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _validate_deps_pex(path: Path) -> None:
|
|
442
|
+
try:
|
|
443
|
+
with zipfile.ZipFile(path) as archive:
|
|
444
|
+
pex_info = json.loads(archive.read("PEX-INFO"))
|
|
445
|
+
except (OSError, KeyError, json.JSONDecodeError, zipfile.BadZipFile) as exc:
|
|
446
|
+
raise ValueError(f"could not inspect built deps.pex: {exc}") from exc
|
|
447
|
+
|
|
448
|
+
distribution_names: set[str] = set()
|
|
449
|
+
for filename in pex_info.get("distributions", {}):
|
|
450
|
+
try:
|
|
451
|
+
name, _version, _build, _tags = parse_wheel_filename(filename)
|
|
452
|
+
except InvalidWheelFilename:
|
|
453
|
+
continue
|
|
454
|
+
distribution_names.add(canonicalize_name(name))
|
|
455
|
+
|
|
456
|
+
missing = [name for name in ("dagster", "dagster-postgres") if name not in distribution_names]
|
|
457
|
+
if missing:
|
|
458
|
+
raise ValueError(
|
|
459
|
+
"built deps.pex does not contain required distribution(s): " + ", ".join(missing)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _setup_py_dependencies(project_path: Path, python: str) -> list[str]:
|
|
464
|
+
setup_py = project_path / "setup.py"
|
|
465
|
+
if not setup_py.exists():
|
|
466
|
+
return []
|
|
467
|
+
|
|
468
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
469
|
+
result = subprocess.run(
|
|
470
|
+
[python, str(setup_py), "egg_info", f"--egg-base={temp_dir}"],
|
|
471
|
+
cwd=project_path,
|
|
472
|
+
capture_output=True,
|
|
473
|
+
text=True,
|
|
474
|
+
check=False,
|
|
475
|
+
)
|
|
476
|
+
if result.returncode:
|
|
477
|
+
raise ValueError(
|
|
478
|
+
"could not inspect setup.py dependencies: "
|
|
479
|
+
+ result.stdout
|
|
480
|
+
+ result.stderr
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
distributions = list(importlib.metadata.distributions(path=[temp_dir]))
|
|
484
|
+
if len(distributions) != 1:
|
|
485
|
+
raise ValueError(f"could not find generated distribution metadata for {setup_py}")
|
|
486
|
+
dependencies: list[str] = []
|
|
487
|
+
for value in distributions[0].requires or []:
|
|
488
|
+
requirement = Requirement(value)
|
|
489
|
+
if requirement.marker and re.search(r"\bextra\b", str(requirement.marker)):
|
|
490
|
+
continue
|
|
491
|
+
dependencies.append(value)
|
|
492
|
+
return dependencies
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _pyproject_dependencies(project_path: Path) -> list[str]:
|
|
496
|
+
pyproject = project_path / "pyproject.toml"
|
|
497
|
+
if not pyproject.exists():
|
|
498
|
+
return []
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
502
|
+
except Exception as exc:
|
|
503
|
+
raise ValueError(f"could not parse pyproject.toml: {exc}") from exc
|
|
504
|
+
|
|
505
|
+
deps: list[str] = []
|
|
506
|
+
project = data.get("project", {})
|
|
507
|
+
deps.extend(str(dep) for dep in project.get("dependencies", []))
|
|
508
|
+
|
|
509
|
+
poetry = data.get("tool", {}).get("poetry", {})
|
|
510
|
+
for dep_name, dep_spec in poetry.get("dependencies", {}).items():
|
|
511
|
+
if dep_name == "python":
|
|
512
|
+
continue
|
|
513
|
+
deps.append(_poetry_dependency(dep_name, dep_spec))
|
|
514
|
+
return [dep for dep in deps if dep]
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _poetry_dependency(dep_name: str, dep_spec: object) -> str:
|
|
518
|
+
if isinstance(dep_spec, str):
|
|
519
|
+
return f"{dep_name}{dep_spec}"
|
|
520
|
+
if isinstance(dep_spec, dict):
|
|
521
|
+
version = dep_spec.get("version", "")
|
|
522
|
+
return f"{dep_name}{version}" if version else dep_name
|
|
523
|
+
return dep_name
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from snowflake.cli.api.plugins.command import (
|
|
4
|
+
SNOWCLI_ROOT_COMMAND_PATH,
|
|
5
|
+
CommandSpec,
|
|
6
|
+
CommandType,
|
|
7
|
+
plugin_hook_impl,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from dagflow_cli import commands
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@plugin_hook_impl
|
|
14
|
+
def command_spec() -> CommandSpec:
|
|
15
|
+
return CommandSpec(
|
|
16
|
+
parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
|
|
17
|
+
command_type=CommandType.COMMAND_GROUP,
|
|
18
|
+
typer_instance=commands.app.create_instance(),
|
|
19
|
+
)
|