projspec 0.0.2__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.
- projspec/__init__.py +7 -0
- projspec/__main__.py +57 -0
- projspec/_version.py +34 -0
- projspec/artifact/__init__.py +18 -0
- projspec/artifact/base.py +114 -0
- projspec/artifact/installable.py +68 -0
- projspec/artifact/linter.py +15 -0
- projspec/artifact/process.py +25 -0
- projspec/artifact/python_env.py +50 -0
- projspec/content/__init__.py +25 -0
- projspec/content/base.py +46 -0
- projspec/content/data.py +24 -0
- projspec/content/env_var.py +10 -0
- projspec/content/environment.py +41 -0
- projspec/content/executable.py +15 -0
- projspec/content/license.py +16 -0
- projspec/content/metadata.py +9 -0
- projspec/content/package.py +10 -0
- projspec/html.py +45 -0
- projspec/proj/__init__.py +31 -0
- projspec/proj/backstage.py +13 -0
- projspec/proj/base.py +323 -0
- projspec/proj/briefcase.py +8 -0
- projspec/proj/conda_package.py +129 -0
- projspec/proj/conda_project.py +119 -0
- projspec/proj/datapackage.py +16 -0
- projspec/proj/documentation.py +45 -0
- projspec/proj/git.py +32 -0
- projspec/proj/helm.py +1 -0
- projspec/proj/hf.py +29 -0
- projspec/proj/node.py +16 -0
- projspec/proj/pixi.py +229 -0
- projspec/proj/poetry.py +82 -0
- projspec/proj/pyscript.py +36 -0
- projspec/proj/python_code.py +169 -0
- projspec/proj/rust.py +34 -0
- projspec/proj/uv.py +167 -0
- projspec/utils.py +357 -0
- projspec-0.0.2.dist-info/METADATA +134 -0
- projspec-0.0.2.dist-info/RECORD +43 -0
- projspec-0.0.2.dist-info/WHEEL +4 -0
- projspec-0.0.2.dist-info/entry_points.txt +2 -0
- projspec-0.0.2.dist-info/licenses/LICENSE +29 -0
projspec/__init__.py
ADDED
projspec/__main__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Simple example executable for this library"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
import projspec.proj
|
|
9
|
+
|
|
10
|
+
# TODO: allow subcommands to execute artifacts, list known types and set config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command()
|
|
14
|
+
@click.option(
|
|
15
|
+
"--types",
|
|
16
|
+
default="ALL",
|
|
17
|
+
help='Type names to scan for (comma-separated list in camel or snake case); defaults to "ALL"',
|
|
18
|
+
)
|
|
19
|
+
@click.argument("path", default=".")
|
|
20
|
+
@click.option("--walk", is_flag=True, help="To descend into all child directories")
|
|
21
|
+
@click.option("--summary", is_flag=True, help="Show abbreviated output")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--make", help="(Re)Create the first artifact found matching this type name"
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--storage_options",
|
|
27
|
+
default="",
|
|
28
|
+
help="storage options dict for the given URL, as JSON",
|
|
29
|
+
)
|
|
30
|
+
def main(path, types, walk, summary, make, storage_options):
|
|
31
|
+
if types in {"ALL", ""}:
|
|
32
|
+
types = None
|
|
33
|
+
else:
|
|
34
|
+
types = types.split(",")
|
|
35
|
+
if storage_options:
|
|
36
|
+
storage_options = json.loads(storage_options)
|
|
37
|
+
else:
|
|
38
|
+
storage_options = None
|
|
39
|
+
proj = projspec.Project(
|
|
40
|
+
path, storage_options=storage_options, types=types, walk=walk
|
|
41
|
+
)
|
|
42
|
+
if make:
|
|
43
|
+
art: projspec.artifact.BaseArtifact
|
|
44
|
+
for art in proj.artifacts:
|
|
45
|
+
if art.snake_name() == projspec.utils.camel_to_snake(make):
|
|
46
|
+
print("Launching:", art)
|
|
47
|
+
art.remake()
|
|
48
|
+
return
|
|
49
|
+
print("No such artifact found")
|
|
50
|
+
elif summary:
|
|
51
|
+
print(proj.text_summary())
|
|
52
|
+
else:
|
|
53
|
+
print(proj)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
projspec/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.0.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 2)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Things that a project can do or make"""
|
|
2
|
+
|
|
3
|
+
from projspec.artifact.base import BaseArtifact, FileArtifact
|
|
4
|
+
from projspec.artifact.installable import CondaPackage, Wheel
|
|
5
|
+
from projspec.artifact.process import Process
|
|
6
|
+
from projspec.artifact.python_env import EnvPack, CondaEnv, VirtualEnv, LockFile
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseArtifact",
|
|
10
|
+
"FileArtifact",
|
|
11
|
+
"CondaPackage",
|
|
12
|
+
"Wheel",
|
|
13
|
+
"Process",
|
|
14
|
+
"EnvPack",
|
|
15
|
+
"CondaEnv",
|
|
16
|
+
"VirtualEnv",
|
|
17
|
+
"LockFile",
|
|
18
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import fsspec.implementations.local
|
|
6
|
+
|
|
7
|
+
from projspec.proj import Project
|
|
8
|
+
from projspec.utils import camel_to_snake, is_installed
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("projspec")
|
|
11
|
+
registry = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseArtifact:
|
|
15
|
+
def __init__(self, proj: Project, cmd: list[str] | None = None):
|
|
16
|
+
self.proj = proj
|
|
17
|
+
self.cmd = cmd
|
|
18
|
+
self.proc = None
|
|
19
|
+
|
|
20
|
+
def _is_clean(self) -> bool:
|
|
21
|
+
return self.proc is None # in general, more complex
|
|
22
|
+
|
|
23
|
+
def _is_done(self) -> bool:
|
|
24
|
+
return self.proc is not None # in general, more complex
|
|
25
|
+
|
|
26
|
+
def _check_runner(self):
|
|
27
|
+
return self.cmd[0] in is_installed
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def state(self) -> Literal["clean", "done", "pending"]:
|
|
31
|
+
if self._is_clean():
|
|
32
|
+
return "clean"
|
|
33
|
+
elif self._is_done():
|
|
34
|
+
return "done"
|
|
35
|
+
else:
|
|
36
|
+
return "pending"
|
|
37
|
+
|
|
38
|
+
def make(self, *args, **kwargs):
|
|
39
|
+
"""Create the artifact and any runtime it depends on"""
|
|
40
|
+
if not isinstance(self.proj.fs, fsspec.implementations.local.LocalFileSystem):
|
|
41
|
+
# Later, will implement download-and-make, although some tools
|
|
42
|
+
# can already do this themselves.
|
|
43
|
+
raise RuntimeError("Can't run local command on remote project")
|
|
44
|
+
logger.debug(" ".join(self.cmd))
|
|
45
|
+
# this default implementation does not store any state
|
|
46
|
+
self._make(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
def _make(self, *args, **kwargs):
|
|
49
|
+
subprocess.check_call(self.cmd, cwd=self.proj.url, **kwargs)
|
|
50
|
+
|
|
51
|
+
def remake(self):
|
|
52
|
+
"""Recreate the artifact and any runtime it depends on"""
|
|
53
|
+
self.clean()
|
|
54
|
+
self.make()
|
|
55
|
+
|
|
56
|
+
def clean(self):
|
|
57
|
+
"""Remove artifact"""
|
|
58
|
+
# this default implementation leaves nothing to clean
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f"{type(self).__name__}, '{' '.join(self.cmd)}', {self.state}"
|
|
63
|
+
|
|
64
|
+
def _repr2(self):
|
|
65
|
+
return f"{' '.join(self.cmd)}, {self.state}"
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def __init_subclass__(cls, **kwargs):
|
|
69
|
+
sn = cls.snake_name()
|
|
70
|
+
if sn in registry:
|
|
71
|
+
raise RuntimeError()
|
|
72
|
+
registry[sn] = cls
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def snake_name(cls):
|
|
76
|
+
return camel_to_snake(cls.__name__)
|
|
77
|
+
|
|
78
|
+
def to_dict(self, compact=True):
|
|
79
|
+
"""Distil the instance to JSON compatible dict
|
|
80
|
+
|
|
81
|
+
compact: if True, will produce condensed output, perhaps justa string.
|
|
82
|
+
"""
|
|
83
|
+
if compact:
|
|
84
|
+
return self._repr2()
|
|
85
|
+
dic = {
|
|
86
|
+
k: v
|
|
87
|
+
for k, v in self.__dict__.items()
|
|
88
|
+
if not k.startswith("_") and k not in ("proj", "proc")
|
|
89
|
+
}
|
|
90
|
+
dic["klass"] = ["artifact", self.snake_name()]
|
|
91
|
+
dic["proc"] = None
|
|
92
|
+
return dic
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_cls(name: str) -> type[BaseArtifact]:
|
|
96
|
+
"""Find an artifact class by snake-case name."""
|
|
97
|
+
return registry[name]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FileArtifact(BaseArtifact):
|
|
101
|
+
"""Specialised artifacts, where the output is one or more files"""
|
|
102
|
+
|
|
103
|
+
# TODO: account for outputs to a directory/glob pattern, so we can
|
|
104
|
+
# apply to wheel; or unknown output location, e.g., conda-build.
|
|
105
|
+
|
|
106
|
+
def __init__(self, proj: Project, fn: str, **kw):
|
|
107
|
+
self.fn = fn
|
|
108
|
+
super().__init__(proj, **kw)
|
|
109
|
+
|
|
110
|
+
def _is_done(self) -> bool:
|
|
111
|
+
return self.proj.fs.exists(self.fn)
|
|
112
|
+
|
|
113
|
+
def _is_clean(self) -> bool:
|
|
114
|
+
return not self.proj.fs.exists(self.fn)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os.path
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from projspec.artifact import BaseArtifact
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("projspec")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Wheel(BaseArtifact):
|
|
11
|
+
"""An installable python wheel file
|
|
12
|
+
|
|
13
|
+
Note that in general there may be a set of wheels for different platforms.
|
|
14
|
+
The actual name of the wheel file depends on platform, vcs config
|
|
15
|
+
and maybe other factors. We just check if the dist/ directory is
|
|
16
|
+
populated.
|
|
17
|
+
|
|
18
|
+
This output is intended to be _local_ - pushing to a remote location (e.g., pypi)
|
|
19
|
+
is call publishing.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def _is_done(self) -> bool:
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
def _is_clean(self) -> bool:
|
|
26
|
+
files = self.proj.fs.glob(f"{self.proj.url}/dist/*.whl")
|
|
27
|
+
return len(files) == 0
|
|
28
|
+
|
|
29
|
+
def clean(self):
|
|
30
|
+
files = self.proj.fs.glob(f"{self.proj.url}/dist/*.whl")
|
|
31
|
+
self.proj.fs.rm(files)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CondaPackage(BaseArtifact):
|
|
35
|
+
"""An installable python wheel file
|
|
36
|
+
|
|
37
|
+
Note that in general, there may be a set of wheels for different platforms.
|
|
38
|
+
The actual name of the wheel file depends on the platform, vcs config
|
|
39
|
+
and maybe other factors. We just check if the dist/ directory is
|
|
40
|
+
populated.
|
|
41
|
+
|
|
42
|
+
This output is intended to be _local_ - pushing to a remote location (e.g., pypi)
|
|
43
|
+
is call publishing.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, path=None, name=None, **kwargs):
|
|
47
|
+
super().__init__(**kwargs)
|
|
48
|
+
self.path: str | None = path
|
|
49
|
+
self.name = name
|
|
50
|
+
|
|
51
|
+
def _make(self, *args, **kwargs):
|
|
52
|
+
import re
|
|
53
|
+
|
|
54
|
+
logger.debug(" ".join(self.cmd))
|
|
55
|
+
out = subprocess.check_output(self.cmd).decode("utf-8")
|
|
56
|
+
if fn := re.match(r"'(.*?\.conda)'\n", out):
|
|
57
|
+
if os.path.exists(fn.group(1)):
|
|
58
|
+
self.path = fn.group(1)
|
|
59
|
+
|
|
60
|
+
def _is_done(self) -> bool:
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def _is_clean(self) -> bool:
|
|
64
|
+
return self.path is None or not self.proj.fs.glob(self.path)
|
|
65
|
+
|
|
66
|
+
def clean(self):
|
|
67
|
+
if self.path is not None:
|
|
68
|
+
self.proj.fs.rm(self.path)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from projspec import Project
|
|
2
|
+
from projspec.artifact import BaseArtifact
|
|
3
|
+
|
|
4
|
+
# ruff, isort, mypy ...
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PreCommit(BaseArtifact):
|
|
8
|
+
"""Typically used as a git hook, this lists a set of linters that a project uses."""
|
|
9
|
+
|
|
10
|
+
# recognised by the presence o .pre-commit-config.yaml, but we don't need
|
|
11
|
+
# to parse it in order to run.
|
|
12
|
+
|
|
13
|
+
def __init__(self, proj: Project, cmd=None):
|
|
14
|
+
# ignore cmd: this should always be the same
|
|
15
|
+
super().__init__(proj, cmd=["pre-commit", "run", "-a"])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from projspec.artifact import BaseArtifact
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Process(BaseArtifact):
|
|
7
|
+
"""A simple process where we know nothing about what it does, only if it's running.
|
|
8
|
+
|
|
9
|
+
Can include batch jobs and long-running services.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def _make(self):
|
|
13
|
+
if self.proc is None:
|
|
14
|
+
self.proc = subprocess.Popen(self.cmd, **self.kw)
|
|
15
|
+
|
|
16
|
+
def _is_done(self) -> bool:
|
|
17
|
+
return self.proc is not None and self.proc.poll() is None
|
|
18
|
+
|
|
19
|
+
def clean(
|
|
20
|
+
self,
|
|
21
|
+
):
|
|
22
|
+
if self.proc is not None:
|
|
23
|
+
self.proc.terminate()
|
|
24
|
+
self.proc.wait()
|
|
25
|
+
self.proc = None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Python runtimes
|
|
2
|
+
|
|
3
|
+
Note that for actually running python processes. There is also an implicit
|
|
4
|
+
runtime from either the env that the process is running in (i.e., the PATH),
|
|
5
|
+
or sys.executable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
from functools import cache
|
|
11
|
+
|
|
12
|
+
from projspec.artifact import FileArtifact
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CondaEnv(FileArtifact):
|
|
16
|
+
"""Path to a project conda-built env
|
|
17
|
+
|
|
18
|
+
Contains both python itself and any other binaries, as well as linked
|
|
19
|
+
libraries.
|
|
20
|
+
|
|
21
|
+
In the case of a project having an environment.yaml with a named output,
|
|
22
|
+
the path may be outside the project tree.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
@cache
|
|
27
|
+
def envs() -> list[str]:
|
|
28
|
+
"""Global conda env root paths"""
|
|
29
|
+
# pixi also has global envs
|
|
30
|
+
out = subprocess.check_output(["conda", "env", "list", "--json"])
|
|
31
|
+
return json.loads(out.decode())["envs"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VirtualEnv(FileArtifact):
|
|
35
|
+
"""Path to a project virtual environment
|
|
36
|
+
|
|
37
|
+
Some tools like pipenv put these environments in a global location.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EnvPack(FileArtifact):
|
|
42
|
+
"""Archival form of a python environment
|
|
43
|
+
|
|
44
|
+
- conda-pack: https://conda.github.io/conda-pack/
|
|
45
|
+
- pixi-pack: https://pixi.sh/latest/deployment/pixi_pack/
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LockFile(FileArtifact):
|
|
50
|
+
"""File containing exact environment specification"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from projspec.content.base import BaseContent
|
|
2
|
+
from projspec.content.data import FrictionlessData, IntakeCatalog
|
|
3
|
+
from projspec.content.env_var import EnvironmentVariables
|
|
4
|
+
from projspec.content.environment import Environment, Stack, Precision
|
|
5
|
+
from projspec.content.executable import Command
|
|
6
|
+
from projspec.content.license import License
|
|
7
|
+
|
|
8
|
+
# from projspec.content.linter
|
|
9
|
+
from projspec.content.metadata import DescriptiveMetadata
|
|
10
|
+
from projspec.content.package import PythonPackage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BaseContent",
|
|
15
|
+
"FrictionlessData",
|
|
16
|
+
"IntakeCatalog",
|
|
17
|
+
"EnvironmentVariables",
|
|
18
|
+
"Command",
|
|
19
|
+
"License",
|
|
20
|
+
"DescriptiveMetadata",
|
|
21
|
+
"PythonPackage",
|
|
22
|
+
"Environment",
|
|
23
|
+
"Stack",
|
|
24
|
+
"Precision",
|
|
25
|
+
]
|
projspec/content/base.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from projspec.artifact import BaseArtifact
|
|
4
|
+
from projspec.proj.base import Project
|
|
5
|
+
from projspec.utils import Enum, camel_to_snake
|
|
6
|
+
|
|
7
|
+
registry = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BaseContent:
|
|
12
|
+
proj: Project = field(repr=False)
|
|
13
|
+
artifacts: set[BaseArtifact] = field(repr=False)
|
|
14
|
+
|
|
15
|
+
def _repr2(self):
|
|
16
|
+
return {
|
|
17
|
+
k: (v.name if isinstance(v, Enum) else v)
|
|
18
|
+
for k, v in self.__dict__.items()
|
|
19
|
+
if not k.startswith("_") and k not in ("proj", "artifacts")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def __init_subclass__(cls, **kwargs):
|
|
24
|
+
sn = cls.snake_name()
|
|
25
|
+
if sn in registry:
|
|
26
|
+
raise RuntimeError()
|
|
27
|
+
registry[sn] = cls
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def snake_name(cls):
|
|
31
|
+
return camel_to_snake(cls.__name__)
|
|
32
|
+
|
|
33
|
+
def to_dict(self, compact=False):
|
|
34
|
+
if compact:
|
|
35
|
+
return self._repr2()
|
|
36
|
+
dic = {
|
|
37
|
+
k: getattr(self, k)
|
|
38
|
+
for k in self.__dataclass_fields__
|
|
39
|
+
if k not in ("proj", "artifacts")
|
|
40
|
+
}
|
|
41
|
+
dic["artifacts"] = []
|
|
42
|
+
dic["klass"] = ["content", self.snake_name()]
|
|
43
|
+
for k in list(dic):
|
|
44
|
+
if isinstance(dic[k], Enum):
|
|
45
|
+
dic[k] = dic[k].value
|
|
46
|
+
return dic
|
projspec/content/data.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Contents specifying datasets"""
|
|
2
|
+
|
|
3
|
+
from projspec.content import BaseContent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FrictionlessData(BaseContent):
|
|
7
|
+
"""A datapackage spec, as defined by frictionlessdata
|
|
8
|
+
|
|
9
|
+
This lists loadable tabular files with defined schema, typically from formats such as
|
|
10
|
+
JSON, CSV, and parquet.
|
|
11
|
+
|
|
12
|
+
See https://specs.frictionlessdata.io/data-package/
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# typically in a datapackage.json spec
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IntakeCatalog(BaseContent):
|
|
19
|
+
"""A catalog of data assets, including basic properties (location) and how to load/process them.
|
|
20
|
+
|
|
21
|
+
See https://intake.readthedocs.io/en/latest/
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# typically in a catalog.yaml free-floating file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from projspec.content.base import BaseContent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class EnvironmentVariables(BaseContent):
|
|
8
|
+
"""A set of environment variable key/value pairs, typically used with new processes."""
|
|
9
|
+
|
|
10
|
+
variables: dict[str, str | None] = field(default_factory=dict)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import auto
|
|
3
|
+
|
|
4
|
+
from projspec.content import BaseContent
|
|
5
|
+
from projspec.utils import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Stack(Enum):
|
|
9
|
+
"""The type of environment by packaging tech"""
|
|
10
|
+
|
|
11
|
+
PIP = auto()
|
|
12
|
+
CONDA = auto()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Precision(Enum):
|
|
16
|
+
"""Type of environment definition by the amount of precision"""
|
|
17
|
+
|
|
18
|
+
# TODO: categories may be refined, e.g., whether items include architecture or hash
|
|
19
|
+
SPEC = auto()
|
|
20
|
+
LOCK = auto()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Environment(BaseContent):
|
|
25
|
+
"""Definition of a python runtime environment"""
|
|
26
|
+
|
|
27
|
+
stack: Stack
|
|
28
|
+
precision: Precision
|
|
29
|
+
packages: list[str]
|
|
30
|
+
# This may be empty for loose specs; may include endpoints or index URLs.
|
|
31
|
+
channels: list[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
def _repr2(self):
|
|
34
|
+
out = {
|
|
35
|
+
k: (v.name if isinstance(v, Enum) else v)
|
|
36
|
+
for k, v in self.__dict__.items()
|
|
37
|
+
if not k.startswith("_") and k not in ("proj", "artifacts")
|
|
38
|
+
}
|
|
39
|
+
if not self.channels:
|
|
40
|
+
out.pop("channels", None)
|
|
41
|
+
return out
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Executable contents produce artifacts"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from projspec.content import BaseContent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Command(BaseContent):
|
|
10
|
+
"""The simplest runnable thing; we don't know what it does/outputs."""
|
|
11
|
+
|
|
12
|
+
cmd: list[str] | str
|
|
13
|
+
|
|
14
|
+
def _repr2(self):
|
|
15
|
+
return " ".join(self.cmd) if isinstance(self.cmd, list) else self.cmd
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from projspec.content import BaseContent
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class License(BaseContent):
|
|
5
|
+
"""A legal description of what the given project (code and other assets) can be used for.
|
|
6
|
+
|
|
7
|
+
This could be one of the typical open-source permissive licenses (see https://spdx.org/licenses/),
|
|
8
|
+
specified either just by its name or by a link. Some projects will have custom or restrictive
|
|
9
|
+
conditions on their replication and use.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# https://opensource.org/licenses
|
|
13
|
+
|
|
14
|
+
shortname: str # aka SPDX
|
|
15
|
+
fullname: str
|
|
16
|
+
url: str # relative in the project or remote HTTP
|
projspec/html.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
def dict_to_html(data: dict, title="Data", open_level=2) -> str:
|
|
2
|
+
"""
|
|
3
|
+
Convert a nested dictionary to expandable HTML using <details> tags.
|
|
4
|
+
|
|
5
|
+
Args:
|
|
6
|
+
data: The dictionary to convert
|
|
7
|
+
title: Title for the details element
|
|
8
|
+
open_level: whether to set elements as expanded; yes if > 0, and will
|
|
9
|
+
decrement for inner levels.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
String containing HTML with expandable details elements
|
|
13
|
+
"""
|
|
14
|
+
# With help from Claude Sonnet 4.
|
|
15
|
+
if not isinstance(data, dict):
|
|
16
|
+
return f"<span>{data}</span>"
|
|
17
|
+
|
|
18
|
+
if not data:
|
|
19
|
+
return ""
|
|
20
|
+
open = "open" if open_level > 0 else "closed"
|
|
21
|
+
|
|
22
|
+
html = [
|
|
23
|
+
f'<details {open} style="margin-left: 20px; margin-bottom: 10px;"><summary style="cursor: pointer; color: #2c5aa0; padding: 5px;"><strong>{title}</strong></summary>'
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for key, value in data.items():
|
|
27
|
+
if isinstance(value, dict):
|
|
28
|
+
html.append(dict_to_html(value, key, open_level - 1))
|
|
29
|
+
elif isinstance(value, (list, tuple)):
|
|
30
|
+
html.append(
|
|
31
|
+
f'<details style="margin-left: 20px; margin-bottom: 10px;"><summary style="cursor: pointer; color: #2c5aa0; padding: 5px;"><strong>{key}</strong></summary>'
|
|
32
|
+
)
|
|
33
|
+
for i, item in enumerate(value):
|
|
34
|
+
if isinstance(item, dict):
|
|
35
|
+
html.append(dict_to_html(item, f"{key}[{i}]", open_level - 1))
|
|
36
|
+
else:
|
|
37
|
+
html.append(f'<div style=" margin: 5px 0;"> {item}</div>')
|
|
38
|
+
html.append("</details>")
|
|
39
|
+
else:
|
|
40
|
+
html.append(
|
|
41
|
+
f'<div style=" margin: 5px 0;"><strong>{key}:</strong> {value}</div>'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
html.append("</details>")
|
|
45
|
+
return "".join(html)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from projspec.proj.base import ParseFailed, Project, ProjectSpec
|
|
2
|
+
from projspec.proj.conda_package import CondaRecipe, RattlerRecipe
|
|
3
|
+
from projspec.proj.conda_project import CondaProject
|
|
4
|
+
from projspec.proj.documentation import RTD, MDBook
|
|
5
|
+
from projspec.proj.git import GitRepo
|
|
6
|
+
from projspec.proj.pixi import Pixi
|
|
7
|
+
from projspec.proj.poetry import Poetry
|
|
8
|
+
from projspec.proj.pyscript import PyScript
|
|
9
|
+
from projspec.proj.python_code import PythonCode, PythonLibrary
|
|
10
|
+
from projspec.proj.rust import Rust, RustPython
|
|
11
|
+
from projspec.proj.uv import Uv
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ParseFailed",
|
|
15
|
+
"Project",
|
|
16
|
+
"ProjectSpec",
|
|
17
|
+
"CondaRecipe",
|
|
18
|
+
"CondaProject",
|
|
19
|
+
"GitRepo",
|
|
20
|
+
"MDBook",
|
|
21
|
+
"Poetry",
|
|
22
|
+
"RattlerRecipe",
|
|
23
|
+
"Pixi",
|
|
24
|
+
"PyScript",
|
|
25
|
+
"PythonCode",
|
|
26
|
+
"PythonLibrary",
|
|
27
|
+
"RTD",
|
|
28
|
+
"Rust",
|
|
29
|
+
"RustPython",
|
|
30
|
+
"Uv",
|
|
31
|
+
]
|