croom 0.1.0__tar.gz

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.
@@ -0,0 +1,24 @@
1
+ name: publish to pypi
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ with:
17
+ fetch-depth: 0
18
+ - uses: actions/setup-python@v7
19
+ with:
20
+ python-version: 3.13
21
+ - run: pip install build
22
+ - run: python -m build
23
+ - run: ls -la dist/
24
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,23 @@
1
+ name: run tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ tests:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: actions/setup-python@v7
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - run: pip install -e ".[dev]"
23
+ - run: pytest -v
croom-0.1.0/.gitignore ADDED
@@ -0,0 +1,220 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # UV
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi/*
127
+ !.pixi/config.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule*
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
201
+ # can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer, you
203
+ # could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
croom-0.1.0/.updv.toml ADDED
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "croom"
3
+ version = "0.1.0"
4
+ previous_version = "0.0.0"
5
+
6
+ [project.files.changelog]
7
+ path = "CHANGELOG.md"
8
+ on-missing-file = "fail"
9
+ on-no-match = "skip"
10
+ pattern = "## \\[Unreleased\\]"
11
+ replacement = "## [{new}] - {date}"
12
+ enabled = true
13
+
14
+ [project.files.initpy]
15
+ path = "src/{name}/__init__.py"
16
+ on-missing-file = "create"
17
+ on-no-match = "write"
18
+ pattern = "__version__ = \"{old}\""
19
+ replacement = "__version__ = \"{new}\""
20
+ enabled = true
21
+
22
+ [project.files.version]
23
+ path = "VERSION"
24
+ on-missing-file = "create"
25
+ on-no-match = "write"
26
+ pattern = "{old}"
27
+ replacement = "{new}"
28
+ enabled = true
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-09-16
9
+
10
+ ### Added
11
+
12
+ - add basic `publish.yml` ci workflow
13
+ - add basic status badge to `README.md`
14
+ - add basic `tests.yml` ci workflow
15
+ - split `room.py` into `room.py` and `roomspec.py`
16
+ - add a basic `roomspec.toml.example`
17
+ - add readme overview
18
+ - add basic test coverage
19
+ - add basic `Checksum`, `Room`, `RoomSpecSource`, `RoomSpecConfiguration`, `PostExtractStep`, and `RoomSpec` classes (`room.py` and `roomspec.py`)
20
+ - add basic project structure
21
+
22
+ ### Changed
23
+
24
+ - rename project from cleanroom to croom
25
+
26
+ ### Fixed
27
+
28
+ ### Removed
@@ -0,0 +1,39 @@
1
+ # contributing to croom
2
+
3
+ croom is maintained on [github](https://github.com/juanvel4000/croom) -- this document explains how to report bugs and send patches there.
4
+
5
+ ## reporting bugs / requesting features
6
+
7
+ use the [issue tracker](https://github.com/juanvel4000/croom/issues). when filing a bug include
8
+
9
+ - your OS and croom version (`pip show croom`)
10
+ - steps to reproduce
11
+ - what you expected vs. what happened
12
+
13
+ ## sending patches
14
+
15
+ 1. fork the repo and make your changes on a branch
16
+ 2. commit with a clear, scoped message (see "commit style" below)
17
+ 3. open a pull request against `main`
18
+
19
+ ## commit style
20
+
21
+ - one logical change per commit
22
+ - imperative, lowercase summary line (`add X`, not `Added X` or `adds X`) -- matches the existing log
23
+ - keep the summary under ~72 chars; use the body for anything that needs more explanation
24
+
25
+ ## before submitting
26
+
27
+ - install dev dependencies: `pip install -e ".[dev]"`
28
+ - run the test suite: `pytest -v`
29
+ - if your change affects behavior, consider whether it needs a test (see `tests/`) or a CHANGELOG.md entry under `[Unreleased]`
30
+
31
+ ## code style
32
+
33
+ - Python >=3.11, type hints expected on public functions
34
+ - see existing code for naming conventions (`snake_case`, docstrings on modules/classes.)
35
+ - format/lint with `ruff format` and `ruff check` before submitting
36
+
37
+ ## license
38
+
39
+ by contributing, you agree your changes are licensed under BSD-3-Clause, matching the rest of the project (see LICENSE).
croom-0.1.0/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+
2
+ Copyright (c) 2026 juanvel400.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
croom-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.5
2
+ Name: croom
3
+ Version: 0.1.0
4
+ Summary: chroot-based linux sandbox cli
5
+ Project-URL: Homepage, https://github.com/juanvel4000/croom
6
+ Project-URL: Repository, https://github.com/juanvel4000/croom
7
+ Project-URL: Issues, https://github.com/juanvel4000/croom/issues
8
+ Project-URL: Changelog, https://github.com/juanvel4000/croom/blob/main/CHANGELOG.md
9
+ Author: juanvel400
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ Keywords: chroot,cli,sandbox
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: BSD License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Requires-Python: >=3.11
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest; extra == 'dev'
25
+ Requires-Dist: ruff; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # croom
29
+
30
+ [![run tests](https://github.com/juanvel4000/croom/actions/workflows/tests.yml/badge.svg)](https://github.com/juanvel4000/croom/actions/workflows/tests.yml)
31
+ [![publish to pypi](https://github.com/juanvel4000/croom/actions/workflows/publish.yml/badge.svg)](https://github.com/juanvel4000/croom/actions/workflows/publish.yml)
32
+
33
+ a simple Linux sandbox CLI.
34
+
35
+ ## overview
36
+
37
+ croom provides a simpler way to create and interact with semi-isolated environments ("`Rooms`"). it uses utilities such as `unshare` and standard Unix tools to construct and provide these environments without root privileges.
38
+
39
+ croom environments are not designed to provide security isolation. instead, they provide a simple space to try, run, and test programs in custom, semi-isolated environments.
40
+
41
+ > croom uses `croom` as the project wide name and `croom` as the distribution name.
42
+
43
+ ## project status
44
+
45
+ croom is in active development. while functional, it is **not yet production-ready** for real-world usage.
46
+
47
+ - [CHANGELOG.md](CHANGELOG.md) -- release history
48
+ - [CONTRIBUTING.md](CONTRIBUTING.md) -- how to report bugs and send patches
49
+
50
+ ## license
51
+
52
+ BSD-3-Clause, see [LICENSE](LICENSE)
croom-0.1.0/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # croom
2
+
3
+ [![run tests](https://github.com/juanvel4000/croom/actions/workflows/tests.yml/badge.svg)](https://github.com/juanvel4000/croom/actions/workflows/tests.yml)
4
+ [![publish to pypi](https://github.com/juanvel4000/croom/actions/workflows/publish.yml/badge.svg)](https://github.com/juanvel4000/croom/actions/workflows/publish.yml)
5
+
6
+ a simple Linux sandbox CLI.
7
+
8
+ ## overview
9
+
10
+ croom provides a simpler way to create and interact with semi-isolated environments ("`Rooms`"). it uses utilities such as `unshare` and standard Unix tools to construct and provide these environments without root privileges.
11
+
12
+ croom environments are not designed to provide security isolation. instead, they provide a simple space to try, run, and test programs in custom, semi-isolated environments.
13
+
14
+ > croom uses `croom` as the project wide name and `croom` as the distribution name.
15
+
16
+ ## project status
17
+
18
+ croom is in active development. while functional, it is **not yet production-ready** for real-world usage.
19
+
20
+ - [CHANGELOG.md](CHANGELOG.md) -- release history
21
+ - [CONTRIBUTING.md](CONTRIBUTING.md) -- how to report bugs and send patches
22
+
23
+ ## license
24
+
25
+ BSD-3-Clause, see [LICENSE](LICENSE)
croom-0.1.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "croom"
3
+ dynamic = ["version"]
4
+ description = "chroot-based linux sandbox cli"
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+
8
+ requires-python = ">=3.11"
9
+
10
+ keywords = [
11
+ "chroot",
12
+ "cli",
13
+ "sandbox"
14
+ ]
15
+
16
+ authors = [
17
+ { name = "juanvel400" }
18
+ ]
19
+
20
+ classifiers = [
21
+ "Development Status :: 2 - Pre-Alpha",
22
+ "Environment :: Console",
23
+ "Intended Audience :: System Administrators",
24
+ "License :: OSI Approved :: BSD License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/juanvel4000/croom"
34
+ Repository = "https://github.com/juanvel4000/croom"
35
+ Issues = "https://github.com/juanvel4000/croom/issues"
36
+ Changelog = "https://github.com/juanvel4000/croom/blob/main/CHANGELOG.md"
37
+
38
+ [tool.hatch.version]
39
+ path = "src/croom/__init__.py"
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [project.optional-dependencies]
46
+ dev = [
47
+ "pytest",
48
+ "ruff",
49
+ ]
50
+
51
+ [project.scripts]
52
+ croom = "croom.cli:main"
@@ -0,0 +1,38 @@
1
+ [roomspec]
2
+ name = "arch"
3
+ version = "2026.09.01"
4
+
5
+ [source]
6
+ url = "https://geo.mirror.pkgbuild.com/iso/2026.09.01/archlinux-bootstrap-x86_64.tar.zst"
7
+ directory = "root.x86_64"
8
+
9
+ [source.checksum]
10
+ type = "sha256"
11
+ value = "895661bdf6c64e91b7725874165fd05dd30c438d3ffec661671ab5cfb261ca58"
12
+
13
+ [configuration]
14
+ symlink-resolv = true
15
+ can-persist = true
16
+ can-cache = true
17
+
18
+ [[postextract]]
19
+ name = "initialize keyring"
20
+ optional = false
21
+ command = "pacman-key --init"
22
+
23
+ [[postextract]]
24
+ name = "populate keyring"
25
+ optional = false
26
+ command = "pacman-key --populate archlinux"
27
+
28
+ [[postextract]]
29
+ name = "update package db"
30
+ optional = false
31
+ command = "pacman -Sy"
32
+
33
+ [[postextract]]
34
+ name = "upgrade the system"
35
+ requires-reload = true
36
+ optional = true
37
+ ignore-fail = true
38
+ command = "pacman -Syu"
@@ -0,0 +1,5 @@
1
+ from .room import Room
2
+ from .roomspec import Checksum, RoomSpec
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["Checksum", "Room", "RoomSpec"]
@@ -0,0 +1,8 @@
1
+ from urllib.parse import urlparse
2
+
3
+
4
+ def is_valid_url(url: str) -> bool:
5
+ """validate an HTTP(S) url"""
6
+ parsed = urlparse(url)
7
+
8
+ return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
@@ -0,0 +1,2 @@
1
+ class Room:
2
+ """represents a room instance"""
@@ -0,0 +1,134 @@
1
+ import hashlib
2
+ import shlex
3
+ import tomllib
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from .helpers import is_valid_url
8
+
9
+
10
+ @dataclass
11
+ class Checksum:
12
+ """represents a checksum to verify a downloaded artifact against"""
13
+
14
+ checksum: str
15
+ type: str = "sha256"
16
+
17
+ def check(self, b: bytes) -> bool:
18
+ """check the stored checksum against bytes"""
19
+ h = hashlib.new(self.type)
20
+ h.update(b)
21
+ return h.hexdigest().strip().lower() == self.checksum.strip().lower()
22
+
23
+
24
+ @dataclass
25
+ class RoomSpecSource:
26
+ checksum: Checksum
27
+ url: str
28
+ directory: Path | None = None
29
+
30
+ def __post_init__(self):
31
+ if not is_valid_url(self.url):
32
+ raise ValueError(f"invalid url: {self.url}")
33
+
34
+ @classmethod
35
+ def from_dict(cls, d: dict) -> "RoomSpecSource":
36
+ """create a RoomSpecSource from a dict"""
37
+ dchecksum = d.get("checksum", {})
38
+ checksum_type = dchecksum.get("type", None)
39
+ checksum_value = dchecksum.get("value", None)
40
+ checksum = Checksum(checksum_value, checksum_type)
41
+
42
+ url = d.get("url", "")
43
+ directory = d.get("directory", None)
44
+
45
+ if not is_valid_url(url):
46
+ raise ValueError(f"invalid url: {url}")
47
+
48
+ return cls(checksum, url, Path(directory) if directory else None)
49
+
50
+
51
+ @dataclass
52
+ class RoomSpecConfiguration:
53
+ symlink_resolv: bool = True
54
+ can_persist: bool = True
55
+ can_cache: bool = False
56
+
57
+ @classmethod
58
+ def from_dict(cls, d: dict) -> "RoomSpecConfiguration":
59
+ """create a RoomSpecConfiguration from a dict"""
60
+ return cls(
61
+ bool(d.get("symlink-resolv", True)),
62
+ bool(d.get("can-persist", True)),
63
+ bool(d.get("can-cache", False)),
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class PostExtractStep:
69
+ name: str
70
+ optional: bool = False
71
+ requires_reload: bool = False
72
+ ignore_fail: bool = False
73
+ command: list[str] = field(default_factory=list)
74
+
75
+ @classmethod
76
+ def from_dict(cls, d: dict) -> "PostExtractStep":
77
+ """create a PostExtractStep from a dict"""
78
+ name = d.get("name", "")
79
+ if not name.strip():
80
+ raise ValueError("name not found")
81
+
82
+ optional = bool(d.get("optional", False))
83
+ requires_reload = bool(d.get("requires-reload", False))
84
+ ignore_fail = bool(d.get("ignore-fail", False))
85
+ scommand = d.get("command", "")
86
+ if not scommand.strip():
87
+ raise ValueError("command not found")
88
+ command = shlex.split(scommand)
89
+
90
+ return cls(name, optional, requires_reload, ignore_fail, command)
91
+
92
+
93
+ @dataclass
94
+ class RoomSpec:
95
+ """represents a room spec"""
96
+
97
+ name: str
98
+ version: str
99
+
100
+ source: RoomSpecSource
101
+ configuration: RoomSpecConfiguration
102
+ postextract: list[PostExtractStep] = field(default_factory=list)
103
+
104
+ @classmethod
105
+ def from_toml(cls, f: Path) -> "RoomSpec":
106
+ """create a RoomSpec from a TOML file"""
107
+ with open(str(f), "rb") as fp:
108
+ d = tomllib.load(fp)
109
+ return cls.from_dict(d)
110
+
111
+ @classmethod
112
+ def from_dict(cls, d: dict) -> "RoomSpec":
113
+ """create a RoomSpec from a dict"""
114
+ name = d.get("roomspec", {}).get("name", "").strip()
115
+ version = d.get("roomspec", {}).get("version", "").strip()
116
+
117
+ if not name:
118
+ raise ValueError("name not found")
119
+
120
+ if not version:
121
+ raise ValueError("version not found")
122
+
123
+ dsrc = d.get("source", {})
124
+ rsrc = RoomSpecSource.from_dict(dsrc)
125
+
126
+ dcfg = d.get("configuration", {})
127
+ rcfg = RoomSpecConfiguration.from_dict(dcfg)
128
+
129
+ dpsteps = d.get("postextract", [])
130
+ psteps = []
131
+ for dpstep in dpsteps:
132
+ psteps.append(PostExtractStep.from_dict(dpstep))
133
+
134
+ return cls(name, version, rsrc, rcfg, psteps)
@@ -0,0 +1,17 @@
1
+ from croom.helpers import is_valid_url
2
+
3
+
4
+ def test_is_valid_url():
5
+ assert is_valid_url("https://example.com")
6
+
7
+
8
+ def test_is_valid_url_garbage():
9
+ assert not is_valid_url("invalid url")
10
+
11
+
12
+ def test_is_valid_url_missing_scheme():
13
+ assert not is_valid_url("example.com")
14
+
15
+
16
+ def test_is_valid_url_missing_authority():
17
+ assert not is_valid_url("https://")
@@ -0,0 +1,128 @@
1
+ import hashlib
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from croom.roomspec import (
7
+ Checksum,
8
+ PostExtractStep,
9
+ RoomSpecConfiguration,
10
+ RoomSpecSource,
11
+ )
12
+
13
+
14
+ @pytest.fixture
15
+ def checksum() -> Checksum:
16
+ return Checksum(
17
+ hashlib.sha256(b"hello world").hexdigest().strip().lower(), "sha256"
18
+ )
19
+
20
+
21
+ def test_checksum_check(checksum: Checksum):
22
+ assert checksum.check(b"hello world")
23
+
24
+
25
+ def test_checksum_check_invalid(checksum: Checksum):
26
+ assert not checksum.check(b"hello, world")
27
+
28
+
29
+ def test_roomspecsource_url_validation_isvalid(checksum):
30
+ assert RoomSpecSource(checksum, "https://example.com").url
31
+
32
+
33
+ def test_roomspecsource_url_validation_missing_scheme(checksum):
34
+ with pytest.raises(ValueError):
35
+ RoomSpecSource(checksum, "example.com")
36
+
37
+
38
+ def test_roomspecsource_url_validation_missing_authority(checksum):
39
+ with pytest.raises(ValueError):
40
+ RoomSpecSource(checksum, "https://")
41
+
42
+
43
+ def test_roomspecsource_url_validation_garbage(checksum):
44
+ with pytest.raises(ValueError):
45
+ RoomSpecSource(checksum, "invalid url")
46
+
47
+
48
+ def test_roomspecsource_from_dict(checksum):
49
+ d = {
50
+ "url": "https://example.com/file.tar.xz",
51
+ "checksum": {"type": checksum.type, "value": checksum.checksum},
52
+ }
53
+
54
+ rsrc = RoomSpecSource.from_dict(d)
55
+ assert rsrc.url == d.get("url", "")
56
+ assert rsrc.checksum.type == d.get("checksum", {}).get("type")
57
+ assert rsrc.checksum.checksum == d.get("checksum", {}).get("value")
58
+ assert not isinstance(rsrc.directory, Path)
59
+ assert rsrc.directory is None
60
+
61
+
62
+ def test_roomspecconfiguration_from_dict():
63
+ d = {"symlink-resolv": False, "can-persist": False, "can-cache": True}
64
+
65
+ rcfg = RoomSpecConfiguration.from_dict(d)
66
+ assert rcfg.symlink_resolv == d.get("symlink-resolv", True)
67
+ assert rcfg.can_persist == d.get("can-persist", True)
68
+ assert rcfg.can_cache == d.get("can-cache", False)
69
+
70
+
71
+ def test_postextractstep_from_dict():
72
+ d = {
73
+ "name": "test_postextractstep_from_dict",
74
+ "optional": True,
75
+ "requires-reload": True,
76
+ "ignore-fail": True,
77
+ "command": 'arg1 --arg2 "arg3 arg3"',
78
+ }
79
+
80
+ pstep = PostExtractStep.from_dict(d)
81
+ assert pstep.name == d.get("name")
82
+ assert pstep.optional == d.get("optional", False)
83
+ assert pstep.requires_reload == d.get("requires-reload", False)
84
+ assert pstep.ignore_fail == d.get("ignore-fail", False)
85
+ assert pstep.command == ["arg1", "--arg2", "arg3 arg3"]
86
+
87
+
88
+ def test_roomspecsource_from_dict_invalid_url(checksum):
89
+ d = {
90
+ "url": "example.com/file.tar.xz",
91
+ "checksum": {"type": checksum.type, "value": checksum.checksum},
92
+ }
93
+
94
+ with pytest.raises(ValueError):
95
+ RoomSpecSource.from_dict(d)
96
+
97
+
98
+ def test_postextractstep_from_dict_command_not_found():
99
+ d = {
100
+ "name": "test_postextractstep_from_dict",
101
+ "optional": True,
102
+ "requires-reload": True,
103
+ "ignore-fail": True,
104
+ "command": "",
105
+ }
106
+
107
+ with pytest.raises(ValueError):
108
+ PostExtractStep.from_dict(d)
109
+
110
+
111
+ def test_postextractstep_from_dict_name_not_found():
112
+ d = {
113
+ "name": "",
114
+ "optional": True,
115
+ "requires-reload": True,
116
+ "ignore-fail": True,
117
+ "command": 'arg1 --arg2 "arg3 arg3"',
118
+ }
119
+
120
+ with pytest.raises(ValueError):
121
+ PostExtractStep.from_dict(d)
122
+
123
+
124
+ def test_roomspecconfiguration_from_dict_defaults():
125
+ rcfg = RoomSpecConfiguration.from_dict({})
126
+ assert rcfg.symlink_resolv is True
127
+ assert rcfg.can_persist is True
128
+ assert rcfg.can_cache is False