env-module-manager 0.7.0__tar.gz → 0.8.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.
Files changed (37) hide show
  1. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/PKG-INFO +12 -1
  2. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/README.md +11 -0
  3. env_module_manager-0.8.0/docs/collective-environments.md +186 -0
  4. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/python-tools.md +70 -3
  5. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/reference.md +9 -0
  6. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/module_manager/__init__.py +1 -1
  7. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/module_manager/cli.py +49 -0
  8. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/module_manager/deploy.py +116 -1
  9. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/pyproject.toml +1 -1
  10. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/tests/test_cli.py +34 -0
  11. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/tests/test_modulefile.py +64 -0
  12. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/uv.lock +1 -1
  13. env_module_manager-0.7.0/docs/collective-environments.md +0 -85
  14. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/.github/workflows/publish.yml +0 -0
  15. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/.github/workflows/tag.yml +0 -0
  16. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/.github/workflows/test.yml +0 -0
  17. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/.gitignore +0 -0
  18. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/.python-version +0 -0
  19. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/.readthedocs.yaml +0 -0
  20. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/AGENTS.md +0 -0
  21. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/CONTRIBUTING.md +0 -0
  22. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/LICENSE.md +0 -0
  23. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/configuration.md +0 -0
  24. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/development.md +0 -0
  25. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/environment-modules.md +0 -0
  26. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/index.md +0 -0
  27. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/installation.md +0 -0
  28. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/rust-tools.md +0 -0
  29. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/shell-tools.md +0 -0
  30. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/stylesheets/extra.css +0 -0
  31. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/docs/uninstall.md +0 -0
  32. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/example.bash +0 -0
  33. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/mkdocs.yml +0 -0
  34. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/module_manager/config.py +0 -0
  35. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/module_manager/modulefile.py +0 -0
  36. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/tests/test_config.py +0 -0
  37. {env_module_manager-0.7.0 → env_module_manager-0.8.0}/tests/test_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: env-module-manager
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Deploy Python, Rust, shell, and collective CLI environments behind GNU environment modulefiles
5
5
  Project-URL: Homepage, https://github.com/ejb90/module-manager
6
6
  Project-URL: Documentation, https://module-manager.readthedocs.io/
@@ -73,6 +73,7 @@ Deploy a Python CLI with `uv tool install`:
73
73
  ```sh
74
74
  module-manager deploy-python ruff 0.8.0 \
75
75
  --package 'ruff==0.8.0' \
76
+ --default-index https://packages.example/simple \
76
77
  --constraints /prod/constraints/runtime.txt \
77
78
  --prefix /prod/tools \
78
79
  --module-root /prod/modulefiles \
@@ -94,6 +95,16 @@ Generate a constraints file for a Python CLI:
94
95
  module-manager auto-constraints ruff==0.8.0 --output constraints.txt
95
96
  ```
96
97
 
98
+ Deploy from a direct wheel or Git URL:
99
+
100
+ ```sh
101
+ module-manager deploy-python my-tool 1.0.0 \
102
+ --package 'my-tool @ file:///prod/wheels/my_tool-1.0.0-py3-none-any.whl' \
103
+ --prefix /prod/tools \
104
+ --module-root /prod/modulefiles \
105
+ --execute-install
106
+ ```
107
+
97
108
  Deploy a shell script:
98
109
 
99
110
  ```sh
@@ -41,6 +41,7 @@ Deploy a Python CLI with `uv tool install`:
41
41
  ```sh
42
42
  module-manager deploy-python ruff 0.8.0 \
43
43
  --package 'ruff==0.8.0' \
44
+ --default-index https://packages.example/simple \
44
45
  --constraints /prod/constraints/runtime.txt \
45
46
  --prefix /prod/tools \
46
47
  --module-root /prod/modulefiles \
@@ -62,6 +63,16 @@ Generate a constraints file for a Python CLI:
62
63
  module-manager auto-constraints ruff==0.8.0 --output constraints.txt
63
64
  ```
64
65
 
66
+ Deploy from a direct wheel or Git URL:
67
+
68
+ ```sh
69
+ module-manager deploy-python my-tool 1.0.0 \
70
+ --package 'my-tool @ file:///prod/wheels/my_tool-1.0.0-py3-none-any.whl' \
71
+ --prefix /prod/tools \
72
+ --module-root /prod/modulefiles \
73
+ --execute-install
74
+ ```
75
+
65
76
  Deploy a shell script:
66
77
 
67
78
  ```sh
@@ -0,0 +1,186 @@
1
+ # Collective Environments
2
+
3
+ Collective environments package several tools behind one modulefile. Use them
4
+ when a shared workflow needs Python CLIs, Rust binaries, and shell scripts to
5
+ appear together from one `module load`.
6
+
7
+ Create a TOML manifest:
8
+
9
+ ```toml
10
+ name = "dev-tools"
11
+ version = "2026.05"
12
+ description = "Shared developer command-line tools"
13
+ prefix = "/prod/tools"
14
+ module_root = "/prod/modulefiles"
15
+ default = true
16
+
17
+ [[tools]]
18
+ type = "python"
19
+ name = "ruff"
20
+ version = "0.8.0"
21
+ package = "ruff==0.8.0"
22
+ python = "3.12"
23
+ indexes = ["https://packages.example/simple"]
24
+ default_index = "https://packages.example/simple"
25
+ find_links = ["/prod/wheels"]
26
+ no_index = false
27
+ index_strategy = "first-index"
28
+ constraints = ["/prod/constraints/runtime.txt"]
29
+ no_cache = false
30
+ refresh = false
31
+ refresh_packages = ["ruff"]
32
+ force = false
33
+ reinstall = false
34
+ uv_config_file = "/prod/config/uv.toml"
35
+
36
+ [[tools]]
37
+ type = "rust"
38
+ name = "ripgrep"
39
+ version = "14.1.1"
40
+ binary = "./target/release/rg"
41
+
42
+ [[tools]]
43
+ type = "script"
44
+ name = "lab-helper"
45
+ version = "1.0.0"
46
+ script = "./scripts/lab-helper"
47
+ ```
48
+
49
+ Deploy the environment:
50
+
51
+ ```sh
52
+ module-manager deploy-env --file dev-tools.toml
53
+ ```
54
+
55
+ This writes one shared environment:
56
+
57
+ ```text
58
+ /prod/tools/dev-tools/2026.05/bin
59
+ /prod/modulefiles/dev-tools/2026.05
60
+ ```
61
+
62
+ The generated modulefile prepends the shared `bin` directory to `PATH`, so all
63
+ tools in the manifest are available after:
64
+
65
+ ```sh
66
+ module use /prod/modulefiles
67
+ module load dev-tools
68
+ ```
69
+
70
+ Relative `binary` and `script` paths are resolved relative to the manifest file.
71
+ Relative Python `constraints` paths are resolved the same way.
72
+
73
+ ## Multiple Python Tools
74
+
75
+ A collective environment can install several Python CLIs into the same shared
76
+ `bin` directory. Each Python tool can use its own package spec and uv resolver
77
+ options:
78
+
79
+ ```toml
80
+ name = "python-dev-tools"
81
+ version = "2026.05"
82
+ prefix = "/prod/tools"
83
+ module_root = "/prod/modulefiles"
84
+
85
+ [[tools]]
86
+ type = "python"
87
+ name = "ruff"
88
+ version = "0.8.0"
89
+ package = "ruff==0.8.0"
90
+ python = "3.12"
91
+ constraints = ["constraints.txt"]
92
+
93
+ [[tools]]
94
+ type = "python"
95
+ name = "gitconductor"
96
+ version = "0.7.0"
97
+ package = "gitconductor==0.7.0"
98
+ default_index = "https://packages.example/simple"
99
+ constraints = ["constraints.txt"]
100
+ refresh_packages = ["gitconductor"]
101
+
102
+ [[tools]]
103
+ type = "python"
104
+ name = "internal-review"
105
+ version = "1.4.2"
106
+ package = "internal-review @ file:///prod/wheels/internal_review-1.4.2-py3-none-any.whl"
107
+ find_links = ["/prod/wheels"]
108
+ no_index = true
109
+ ```
110
+
111
+ Deploy it with:
112
+
113
+ ```sh
114
+ module-manager deploy-env --file python-dev-tools.toml
115
+ ```
116
+
117
+ After `module load python-dev-tools`, the `ruff`, `gitconductor`, and
118
+ `internal-review` executables are available from one module.
119
+
120
+ ## Python Tools and Bash Scripts
121
+
122
+ Use script entries for Bash helpers that should live alongside Python CLIs:
123
+
124
+ ```toml
125
+ name = "release-tools"
126
+ version = "2026.05"
127
+ description = "Release automation helpers"
128
+ prefix = "/prod/tools"
129
+ module_root = "/prod/modulefiles"
130
+
131
+ [[tools]]
132
+ type = "python"
133
+ name = "gitconductor"
134
+ version = "0.7.0"
135
+ package = "gitconductor==0.7.0"
136
+ constraints = ["constraints.txt"]
137
+
138
+ [[tools]]
139
+ type = "python"
140
+ name = "changelog-builder"
141
+ version = "2.1.0"
142
+ package = "changelog-builder @ git+https://github.com/org/changelog-builder.git@v2.1.0"
143
+
144
+ [[tools]]
145
+ type = "script"
146
+ name = "release-check"
147
+ version = "1.0.0"
148
+ script = "./scripts/release-check.sh"
149
+
150
+ [[tools]]
151
+ type = "script"
152
+ name = "publish-docs"
153
+ version = "1.0.0"
154
+ script = "./scripts/publish-docs.sh"
155
+ ```
156
+
157
+ Deploy it with:
158
+
159
+ ```sh
160
+ module-manager deploy-env --file release-tools.toml
161
+ ```
162
+
163
+ The script files are copied into the shared environment `bin` directory and
164
+ marked executable, so they appear on `PATH` next to the Python entry points.
165
+
166
+ ## Preview
167
+
168
+ Use `--dry-run` to preview the install, copy, and modulefile actions without
169
+ creating files:
170
+
171
+ ```sh
172
+ module-manager deploy-env --file dev-tools.toml --dry-run
173
+ ```
174
+
175
+ ## Overrides
176
+
177
+ The manifest can define `prefix`, `module_root`, and `default`, but CLI options
178
+ can override them:
179
+
180
+ ```sh
181
+ module-manager deploy-env \
182
+ --file dev-tools.toml \
183
+ --prefix /scratch/tools \
184
+ --module-root /scratch/modulefiles \
185
+ --no-default
186
+ ```
@@ -34,20 +34,37 @@ unchanged.
34
34
  ```sh
35
35
  module-manager deploy-python gitconductor 0.1.0 \
36
36
  --package gitconductor==0.1.0 \
37
+ --default-index https://packages.example/simple \
37
38
  --index https://packages.example/simple \
38
39
  --prefix /prod/tools \
39
40
  --module-root /prod/modulefiles \
40
41
  --execute-install
41
42
  ```
42
43
 
43
- `--index` may be used more than once.
44
+ `--index` may be used more than once. Use `--default-index` to replace uv's
45
+ default package index, or `--no-index` when resolution should use only direct
46
+ URLs and `--find-links` locations.
44
47
 
45
48
  ## Deploy from a Wheelhouse
46
49
 
50
+ Use `--find-links` when wheels are staged in a directory or simple HTML page:
51
+
52
+ ```sh
53
+ module-manager deploy-python gitconductor 0.1.0 \
54
+ --package gitconductor==0.1.0 \
55
+ --find-links /prod/wheels \
56
+ --prefix /prod/tools \
57
+ --module-root /prod/modulefiles \
58
+ --execute-install
59
+ ```
60
+
61
+ Add `--no-index` to make the install fully wheelhouse-only:
62
+
47
63
  ```sh
48
64
  module-manager deploy-python gitconductor 0.1.0 \
49
65
  --package gitconductor==0.1.0 \
50
66
  --find-links /prod/wheels \
67
+ --no-index \
51
68
  --prefix /prod/tools \
52
69
  --module-root /prod/modulefiles \
53
70
  --execute-install
@@ -55,6 +72,28 @@ module-manager deploy-python gitconductor 0.1.0 \
55
72
 
56
73
  `--find-links` may be used more than once.
57
74
 
75
+ ## Deploy from a Wheel URL
76
+
77
+ Pass a direct wheel URL or file URL as the package spec:
78
+
79
+ ```sh
80
+ module-manager deploy-python gitconductor 0.1.0 \
81
+ --package 'gitconductor @ https://packages.example/files/gitconductor-0.1.0-py3-none-any.whl' \
82
+ --prefix /prod/tools \
83
+ --module-root /prod/modulefiles \
84
+ --execute-install
85
+ ```
86
+
87
+ For a local wheel file:
88
+
89
+ ```sh
90
+ module-manager deploy-python gitconductor 0.1.0 \
91
+ --package 'gitconductor @ file:///prod/wheels/gitconductor-0.1.0-py3-none-any.whl' \
92
+ --prefix /prod/tools \
93
+ --module-root /prod/modulefiles \
94
+ --execute-install
95
+ ```
96
+
58
97
  ## Use Constraint Files
59
98
 
60
99
  Pass requirement constraint files to `uv tool install`:
@@ -70,6 +109,24 @@ module-manager deploy-python gitconductor 0.1.0 \
70
109
 
71
110
  `--constraints` may be used more than once.
72
111
 
112
+ ## Cache and Refresh Controls
113
+
114
+ Pass uv cache controls through when you need fresh index or package metadata:
115
+
116
+ ```sh
117
+ module-manager deploy-python gitconductor 0.1.0 \
118
+ --package gitconductor==0.1.0 \
119
+ --refresh \
120
+ --refresh-package gitconductor \
121
+ --no-cache \
122
+ --prefix /prod/tools \
123
+ --module-root /prod/modulefiles \
124
+ --execute-install
125
+ ```
126
+
127
+ Use `--force` or `--reinstall` when re-running an install into an existing
128
+ versioned tool environment.
129
+
73
130
  ## Generate Constraint Files
74
131
 
75
132
  Use `auto-constraints` to compile a package and its dependencies into a
@@ -111,8 +168,8 @@ through the environment.
111
168
 
112
169
  ## Deploy from VCS
113
170
 
114
- `--package` is passed directly to `uv tool install`, so VCS package specs are
115
- supported.
171
+ `--package` is passed directly to `uv tool install`, so Git package specs are
172
+ supported. Pin a tag, branch, or commit to keep deployments reproducible.
116
173
 
117
174
  ```sh
118
175
  module-manager deploy-python mytool 1.0.0 \
@@ -122,6 +179,16 @@ module-manager deploy-python mytool 1.0.0 \
122
179
  --execute-install
123
180
  ```
124
181
 
182
+ For an SSH Git URL:
183
+
184
+ ```sh
185
+ module-manager deploy-python mytool 1.0.0 \
186
+ --package 'git+ssh://git@github.com/org/repo.git@v1.0.0' \
187
+ --prefix /prod/tools \
188
+ --module-root /prod/modulefiles \
189
+ --execute-install
190
+ ```
191
+
125
192
  For a package in a repository subdirectory:
126
193
 
127
194
  ```sh
@@ -49,8 +49,17 @@ module-manager deploy-python NAME VERSION \
49
49
  --package PACKAGE \
50
50
  [--python PYTHON] \
51
51
  [--index URL] \
52
+ [--default-index URL] \
52
53
  [--find-links PATH_OR_URL] \
54
+ [--no-index] \
55
+ [--index-strategy STRATEGY] \
56
+ [--keyring-provider PROVIDER] \
53
57
  [--constraints PATH] \
58
+ [--no-cache] \
59
+ [--refresh] \
60
+ [--refresh-package PACKAGE] \
61
+ [--force] \
62
+ [--reinstall] \
54
63
  [--uv-config-file PATH] \
55
64
  [--prefix PATH] \
56
65
  [--module-root PATH] \
@@ -1,3 +1,3 @@
1
1
  """Deployment helpers for HPC environment module files."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.8.0"
@@ -203,18 +203,40 @@ def main(ctx: click.Context, config: Path | None) -> None:
203
203
  multiple=True,
204
204
  help="Additional package index URL passed to uv. May be used more than once.",
205
205
  )
206
+ @click.option("--default-index", help="Default package index URL passed to uv.")
206
207
  @click.option(
207
208
  "--find-links",
208
209
  "find_links",
209
210
  multiple=True,
210
211
  help="Directory or HTML page of packages passed to uv. May be used more than once.",
211
212
  )
213
+ @click.option("--no-index", is_flag=True, help="Ignore registry indexes and use direct URLs or find-links.")
214
+ @click.option(
215
+ "--index-strategy",
216
+ type=click.Choice(["first-index", "unsafe-first-match", "unsafe-best-match"]),
217
+ help="Package index strategy passed to uv.",
218
+ )
219
+ @click.option(
220
+ "--keyring-provider",
221
+ type=click.Choice(["disabled", "subprocess"]),
222
+ help="Keyring provider passed to uv.",
223
+ )
212
224
  @click.option(
213
225
  "--constraints",
214
226
  "constraints",
215
227
  multiple=True,
216
228
  help="Requirements constraint file passed to uv. May be used more than once.",
217
229
  )
230
+ @click.option("--no-cache", is_flag=True, help="Avoid reading from or writing to the uv cache.")
231
+ @click.option("--refresh", is_flag=True, help="Refresh all uv cached data.")
232
+ @click.option(
233
+ "--refresh-package",
234
+ "refresh_packages",
235
+ multiple=True,
236
+ help="Refresh cached data for a package. May be used more than once.",
237
+ )
238
+ @click.option("--force", is_flag=True, help="Replace existing executable entries.")
239
+ @click.option("--reinstall", is_flag=True, help="Reinstall all packages in the tool environment.")
218
240
  @click.option(
219
241
  "--uv-config-file",
220
242
  type=PATH,
@@ -238,8 +260,17 @@ def deploy_python(
238
260
  package: str,
239
261
  python: str | None,
240
262
  indexes: tuple[str, ...],
263
+ default_index: str | None,
241
264
  find_links: tuple[str, ...],
265
+ no_index: bool,
266
+ index_strategy: str | None,
267
+ keyring_provider: str | None,
242
268
  constraints: tuple[str, ...],
269
+ no_cache: bool,
270
+ refresh: bool,
271
+ refresh_packages: tuple[str, ...],
272
+ force: bool,
273
+ reinstall: bool,
243
274
  uv_config_file: Path | None,
244
275
  execute_install: bool,
245
276
  ) -> None:
@@ -257,8 +288,17 @@ def deploy_python(
257
288
  package: Package spec passed to `uv tool install`.
258
289
  python: Optional Python interpreter or version passed to uv.
259
290
  indexes: Additional package index URLs passed to uv.
291
+ default_index: Default package index URL passed to uv.
260
292
  find_links: Wheelhouse directories or HTML package pages passed to uv.
293
+ no_index: Whether uv should ignore registry indexes.
294
+ index_strategy: Package index strategy passed to uv.
295
+ keyring_provider: Keyring provider passed to uv.
261
296
  constraints: Requirements constraint files passed to uv.
297
+ no_cache: Whether uv should avoid reading from or writing to cache.
298
+ refresh: Whether uv should refresh cached data.
299
+ refresh_packages: Packages whose cached data uv should refresh.
300
+ force: Whether uv should replace existing executable entries.
301
+ reinstall: Whether uv should reinstall all packages.
262
302
  uv_config_file: Optional uv configuration file passed to `uv tool`.
263
303
  execute_install: Whether to run `uv tool install` immediately.
264
304
 
@@ -282,8 +322,17 @@ def deploy_python(
282
322
  homepage=homepage,
283
323
  python=python,
284
324
  indexes=indexes or config.indexes,
325
+ default_index=default_index,
285
326
  find_links=find_links or config.find_links,
327
+ no_index=no_index,
328
+ index_strategy=index_strategy,
329
+ keyring_provider=keyring_provider,
286
330
  constraints=constraints,
331
+ no_cache=no_cache,
332
+ refresh=refresh,
333
+ refresh_packages=refresh_packages,
334
+ force=force,
335
+ reinstall=reinstall,
287
336
  uv_config_file=(uv_config_file.expanduser() if uv_config_file else config.uv_config_file),
288
337
  execute_install=execute_install,
289
338
  make_default=make_default,
@@ -72,8 +72,17 @@ class EnvironmentToolSpec:
72
72
  script: Shell script path to copy into the shared `bin` directory.
73
73
  python: Optional Python interpreter or version passed to uv.
74
74
  indexes: Additional package index URLs passed to uv.
75
+ default_index: Default package index URL passed to uv.
75
76
  find_links: Wheelhouse directories or HTML package pages passed to uv.
77
+ no_index: Whether uv should ignore registry indexes.
78
+ index_strategy: Package index strategy passed to uv.
79
+ keyring_provider: Keyring provider passed to uv.
76
80
  constraints: Constraint requirement files passed to uv.
81
+ no_cache: Whether uv should avoid reading from or writing to cache.
82
+ refresh: Whether uv should refresh cached data.
83
+ refresh_packages: Packages whose cached data uv should refresh.
84
+ force: Whether uv should replace existing executable entries.
85
+ reinstall: Whether uv should reinstall all packages.
77
86
  description: Optional tool description.
78
87
  homepage: Optional upstream homepage.
79
88
  """
@@ -87,8 +96,17 @@ class EnvironmentToolSpec:
87
96
  script: Path | None = None
88
97
  python: str | None = None
89
98
  indexes: tuple[str, ...] = ()
99
+ default_index: str | None = None
90
100
  find_links: tuple[str, ...] = ()
101
+ no_index: bool = False
102
+ index_strategy: str | None = None
103
+ keyring_provider: str | None = None
91
104
  constraints: tuple[str, ...] = ()
105
+ no_cache: bool = False
106
+ refresh: bool = False
107
+ refresh_packages: tuple[str, ...] = ()
108
+ force: bool = False
109
+ reinstall: bool = False
92
110
  description: str | None = None
93
111
  homepage: str | None = None
94
112
 
@@ -176,8 +194,17 @@ def uv_install_command(
176
194
  package: str,
177
195
  python: str | None = None,
178
196
  indexes: tuple[str, ...] = (),
197
+ default_index: str | None = None,
179
198
  find_links: tuple[str, ...] = (),
199
+ no_index: bool = False,
200
+ index_strategy: str | None = None,
201
+ keyring_provider: str | None = None,
180
202
  constraints: tuple[str, ...] = (),
203
+ no_cache: bool = False,
204
+ refresh: bool = False,
205
+ refresh_packages: tuple[str, ...] = (),
206
+ force: bool = False,
207
+ reinstall: bool = False,
181
208
  uv_config_file: Path | None = None,
182
209
  ) -> list[str]:
183
210
  """Build the uv command used to install a Python CLI tool.
@@ -186,8 +213,17 @@ def uv_install_command(
186
213
  package: Package spec passed to `uv tool install`.
187
214
  python: Optional Python interpreter or version passed to uv.
188
215
  indexes: Additional package index URLs.
216
+ default_index: Default package index URL.
189
217
  find_links: Wheelhouse directories or HTML package pages.
218
+ no_index: Whether to ignore registry indexes.
219
+ index_strategy: Package index strategy.
220
+ keyring_provider: Keyring provider.
190
221
  constraints: Constraint requirement files.
222
+ no_cache: Whether to avoid reading from or writing to cache.
223
+ refresh: Whether to refresh cached data.
224
+ refresh_packages: Packages whose cached data uv should refresh.
225
+ force: Whether to replace existing executable entries.
226
+ reinstall: Whether to reinstall all packages.
191
227
  uv_config_file: Optional uv configuration file passed to `uv tool`.
192
228
 
193
229
  Returns:
@@ -201,10 +237,28 @@ def uv_install_command(
201
237
  command.extend(["--python", python])
202
238
  for index in indexes:
203
239
  command.extend(["--index", index])
240
+ if default_index:
241
+ command.extend(["--default-index", default_index])
204
242
  for link in find_links:
205
243
  command.extend(["--find-links", link])
244
+ if no_index:
245
+ command.append("--no-index")
246
+ if index_strategy:
247
+ command.extend(["--index-strategy", index_strategy])
248
+ if keyring_provider:
249
+ command.extend(["--keyring-provider", keyring_provider])
206
250
  for constraint in constraints:
207
251
  command.extend(["--constraints", constraint])
252
+ if no_cache:
253
+ command.append("--no-cache")
254
+ if refresh:
255
+ command.append("--refresh")
256
+ for package_name in refresh_packages:
257
+ command.extend(["--refresh-package", package_name])
258
+ if force:
259
+ command.append("--force")
260
+ if reinstall:
261
+ command.append("--reinstall")
208
262
  command.append(package)
209
263
  return command
210
264
 
@@ -688,8 +742,17 @@ def parse_environment_tool(data: object, base_dir: Path, index: int) -> Environm
688
742
  script=optional_manifest_path(data, "script", base_dir),
689
743
  python=optional_string(data, "python"),
690
744
  indexes=optional_string_tuple(data, "indexes"),
745
+ default_index=optional_string(data, "default_index"),
691
746
  find_links=optional_string_tuple(data, "find_links"),
747
+ no_index=optional_bool(data, "no_index", False),
748
+ index_strategy=optional_string(data, "index_strategy"),
749
+ keyring_provider=optional_string(data, "keyring_provider"),
692
750
  constraints=tuple(str(path) for path in optional_manifest_paths(data, "constraints", base_dir)),
751
+ no_cache=optional_bool(data, "no_cache", False),
752
+ refresh=optional_bool(data, "refresh", False),
753
+ refresh_packages=optional_string_tuple(data, "refresh_packages"),
754
+ force=optional_bool(data, "force", False),
755
+ reinstall=optional_bool(data, "reinstall", False),
693
756
  description=optional_string(data, "description"),
694
757
  homepage=optional_string(data, "homepage"),
695
758
  )
@@ -747,8 +810,17 @@ def environment_actions(spec: EnvironmentSpec, paths: DeploymentPaths) -> tuple[
747
810
  tool.package,
748
811
  tool.python,
749
812
  tool.indexes,
813
+ tool.default_index,
750
814
  tool.find_links,
815
+ tool.no_index,
816
+ tool.index_strategy,
817
+ tool.keyring_provider,
751
818
  tool.constraints,
819
+ tool.no_cache,
820
+ tool.refresh,
821
+ tool.refresh_packages,
822
+ tool.force,
823
+ tool.reinstall,
752
824
  tool.uv_config_file,
753
825
  )
754
826
  actions.append(
@@ -806,8 +878,17 @@ def deploy_environment(
806
878
  tool.package,
807
879
  tool.python,
808
880
  tool.indexes,
881
+ tool.default_index,
809
882
  tool.find_links,
883
+ tool.no_index,
884
+ tool.index_strategy,
885
+ tool.keyring_provider,
810
886
  tool.constraints,
887
+ tool.no_cache,
888
+ tool.refresh,
889
+ tool.refresh_packages,
890
+ tool.force,
891
+ tool.reinstall,
811
892
  tool.uv_config_file,
812
893
  ),
813
894
  check=True,
@@ -847,8 +928,17 @@ def deploy_python_tool(
847
928
  homepage: str | None = None,
848
929
  python: str | None = None,
849
930
  indexes: tuple[str, ...] = (),
931
+ default_index: str | None = None,
850
932
  find_links: tuple[str, ...] = (),
933
+ no_index: bool = False,
934
+ index_strategy: str | None = None,
935
+ keyring_provider: str | None = None,
851
936
  constraints: tuple[str, ...] = (),
937
+ no_cache: bool = False,
938
+ refresh: bool = False,
939
+ refresh_packages: tuple[str, ...] = (),
940
+ force: bool = False,
941
+ reinstall: bool = False,
852
942
  uv_config_file: Path | None = None,
853
943
  execute_install: bool = False,
854
944
  make_default: bool = True,
@@ -865,8 +955,17 @@ def deploy_python_tool(
865
955
  homepage: Optional upstream homepage shown in module help.
866
956
  python: Optional Python interpreter or version passed to uv.
867
957
  indexes: Additional package index URLs passed to uv.
958
+ default_index: Default package index URL passed to uv.
868
959
  find_links: Wheelhouse directories or HTML package pages passed to uv.
960
+ no_index: Whether uv should ignore registry indexes.
961
+ index_strategy: Package index strategy passed to uv.
962
+ keyring_provider: Keyring provider passed to uv.
869
963
  constraints: Constraint requirement files passed to uv.
964
+ no_cache: Whether uv should avoid reading from or writing to cache.
965
+ refresh: Whether uv should refresh cached data.
966
+ refresh_packages: Packages whose cached data uv should refresh.
967
+ force: Whether uv should replace existing executable entries.
968
+ reinstall: Whether uv should reinstall all packages.
870
969
  uv_config_file: Optional uv configuration file passed to `uv tool`.
871
970
  execute_install: Whether to run `uv tool install` immediately.
872
971
  make_default: Whether to make this version the module default.
@@ -882,7 +981,23 @@ def deploy_python_tool(
882
981
  paths = deployment_paths(module_root, prefix, name, version)
883
982
  paths.bin_dir.mkdir(parents=True, exist_ok=True)
884
983
  tool_dir = paths.install_root / "uv-tools"
885
- command = uv_install_command(package, python, indexes, find_links, constraints, uv_config_file)
984
+ command = uv_install_command(
985
+ package,
986
+ python,
987
+ indexes,
988
+ default_index,
989
+ find_links,
990
+ no_index,
991
+ index_strategy,
992
+ keyring_provider,
993
+ constraints,
994
+ no_cache,
995
+ refresh,
996
+ refresh_packages,
997
+ force,
998
+ reinstall,
999
+ uv_config_file,
1000
+ )
886
1001
 
887
1002
  if execute_install:
888
1003
  require_executable("uv")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "env-module-manager"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Deploy Python, Rust, shell, and collective CLI environments behind GNU environment modulefiles"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -56,10 +56,23 @@ def test_deploy_python_command_writes_modulefile() -> None:
56
56
  "modules",
57
57
  "--index",
58
58
  "https://packages.example/simple",
59
+ "--default-index",
60
+ "https://default.example/simple",
59
61
  "--find-links",
60
62
  "/prod/wheels",
63
+ "--no-index",
64
+ "--index-strategy",
65
+ "unsafe-first-match",
66
+ "--keyring-provider",
67
+ "subprocess",
61
68
  "--constraints",
62
69
  "/prod/constraints.txt",
70
+ "--no-cache",
71
+ "--refresh",
72
+ "--refresh-package",
73
+ "ruff",
74
+ "--force",
75
+ "--reinstall",
63
76
  "--uv-config-file",
64
77
  "/prod/uv.toml",
65
78
  ],
@@ -70,7 +83,16 @@ def test_deploy_python_command_writes_modulefile() -> None:
70
83
  assert "modulefile: modules/ruff/0.8.0" in result.output
71
84
  assert "default version: modules/ruff/.version" in result.output
72
85
  assert "--config-file /prod/uv.toml" in modulefile
86
+ assert "--default-index https://default.example/simple" in modulefile
87
+ assert "--no-index" in modulefile
88
+ assert "--index-strategy unsafe-first-match" in modulefile
89
+ assert "--keyring-provider subprocess" in modulefile
73
90
  assert "--constraints /prod/constraints.txt" in modulefile
91
+ assert "--no-cache" in modulefile
92
+ assert "--refresh" in modulefile
93
+ assert "--refresh-package ruff" in modulefile
94
+ assert "--force" in modulefile
95
+ assert "--reinstall" in modulefile
74
96
 
75
97
 
76
98
  def test_deploy_python_command_uses_config_defaults() -> None:
@@ -463,7 +485,13 @@ name = "ruff"
463
485
  version = "0.8.0"
464
486
  package = "ruff==0.8.0"
465
487
  indexes = ["https://packages.example/simple"]
488
+ default_index = "https://default.example/simple"
489
+ no_index = true
466
490
  constraints = ["constraints.txt"]
491
+ no_cache = true
492
+ refresh = true
493
+ refresh_packages = ["ruff"]
494
+ force = true
467
495
  """.strip(),
468
496
  encoding="utf-8",
469
497
  )
@@ -474,7 +502,13 @@ constraints = ["constraints.txt"]
474
502
  assert "would create install root: tools/dev-tools/2026.05" in result.output
475
503
  assert "would install python ruff:" in result.output
476
504
  assert "--index https://packages.example/simple" in result.output
505
+ assert "--default-index https://default.example/simple" in result.output
506
+ assert "--no-index" in result.output
477
507
  assert "--constraints constraints.txt" in result.output
508
+ assert "--no-cache" in result.output
509
+ assert "--refresh" in result.output
510
+ assert "--refresh-package ruff" in result.output
511
+ assert "--force" in result.output
478
512
  assert not install_root_exists
479
513
 
480
514
 
@@ -88,8 +88,17 @@ def test_python_tool_install_command_accepts_indexes() -> None:
88
88
  assert uv_install_command(
89
89
  "internal-tool==1.2.3",
90
90
  indexes=("https://packages.example/simple", "https://mirror.example/simple"),
91
+ default_index="https://default.example/simple",
91
92
  find_links=("/prod/wheels",),
93
+ no_index=True,
94
+ index_strategy="unsafe-best-match",
95
+ keyring_provider="subprocess",
92
96
  constraints=("/prod/constraints.txt",),
97
+ no_cache=True,
98
+ refresh=True,
99
+ refresh_packages=("internal-tool",),
100
+ force=True,
101
+ reinstall=True,
93
102
  ) == [
94
103
  "uv",
95
104
  "tool",
@@ -98,10 +107,23 @@ def test_python_tool_install_command_accepts_indexes() -> None:
98
107
  "https://packages.example/simple",
99
108
  "--index",
100
109
  "https://mirror.example/simple",
110
+ "--default-index",
111
+ "https://default.example/simple",
101
112
  "--find-links",
102
113
  "/prod/wheels",
114
+ "--no-index",
115
+ "--index-strategy",
116
+ "unsafe-best-match",
117
+ "--keyring-provider",
118
+ "subprocess",
103
119
  "--constraints",
104
120
  "/prod/constraints.txt",
121
+ "--no-cache",
122
+ "--refresh",
123
+ "--refresh-package",
124
+ "internal-tool",
125
+ "--force",
126
+ "--reinstall",
105
127
  "internal-tool==1.2.3",
106
128
  ]
107
129
 
@@ -268,16 +290,26 @@ def test_deploy_python_tool_writes_package_sources_to_install_hint(tmp_path: Pat
268
290
  module_root=tmp_path / "modules",
269
291
  prefix=tmp_path / "tools",
270
292
  indexes=("https://packages.example/simple",),
293
+ default_index="https://default.example/simple",
271
294
  find_links=("/prod/wheels",),
295
+ index_strategy="first-index",
296
+ keyring_provider="disabled",
272
297
  constraints=("/prod/constraints.txt",),
298
+ refresh_packages=("internal-tool",),
299
+ force=True,
273
300
  uv_config_file=Path("/prod/uv.toml"),
274
301
  )
275
302
 
276
303
  modulefile = paths.modulefile.read_text(encoding="utf-8")
277
304
  assert "--config-file /prod/uv.toml" in modulefile
278
305
  assert "--index https://packages.example/simple" in modulefile
306
+ assert "--default-index https://default.example/simple" in modulefile
279
307
  assert "--find-links /prod/wheels" in modulefile
308
+ assert "--index-strategy first-index" in modulefile
309
+ assert "--keyring-provider disabled" in modulefile
280
310
  assert "--constraints /prod/constraints.txt" in modulefile
311
+ assert "--refresh-package internal-tool" in modulefile
312
+ assert "--force" in modulefile
281
313
 
282
314
 
283
315
  def test_deploy_python_tool_writes_url_dependency_to_install_hint(tmp_path: Path) -> None:
@@ -536,8 +568,17 @@ version = "0.8.0"
536
568
  package = "ruff==0.8.0"
537
569
  python = "3.12"
538
570
  indexes = ["https://packages.example/simple"]
571
+ default_index = "https://default.example/simple"
539
572
  find_links = ["/prod/wheels"]
573
+ no_index = true
574
+ index_strategy = "unsafe-best-match"
575
+ keyring_provider = "subprocess"
540
576
  constraints = ["constraints.txt", "/prod/global-constraints.txt"]
577
+ no_cache = true
578
+ refresh = true
579
+ refresh_packages = ["ruff"]
580
+ force = true
581
+ reinstall = true
541
582
  uv_config_file = "uv.toml"
542
583
 
543
584
  [[tools]]
@@ -557,7 +598,16 @@ script = "scripts/helper"
557
598
  assert not spec.make_default
558
599
  assert spec.tools[0].package == "ruff==0.8.0"
559
600
  assert spec.tools[0].indexes == ("https://packages.example/simple",)
601
+ assert spec.tools[0].default_index == "https://default.example/simple"
602
+ assert spec.tools[0].no_index
603
+ assert spec.tools[0].index_strategy == "unsafe-best-match"
604
+ assert spec.tools[0].keyring_provider == "subprocess"
560
605
  assert spec.tools[0].constraints == (str(tmp_path / "constraints.txt"), "/prod/global-constraints.txt")
606
+ assert spec.tools[0].no_cache
607
+ assert spec.tools[0].refresh
608
+ assert spec.tools[0].refresh_packages == ("ruff",)
609
+ assert spec.tools[0].force
610
+ assert spec.tools[0].reinstall
561
611
  assert spec.tools[0].uv_config_file == tmp_path / "uv.toml"
562
612
  assert spec.tools[1].script == tmp_path / "scripts/helper"
563
613
 
@@ -666,7 +716,13 @@ type = "python"
666
716
  name = "ruff"
667
717
  package = "ruff==0.8.0"
668
718
  python = "3.12"
719
+ default_index = "https://default.example/simple"
720
+ no_index = true
669
721
  constraints = ["constraints.txt"]
722
+ no_cache = true
723
+ refresh = true
724
+ refresh_packages = ["ruff"]
725
+ force = true
670
726
  uv_config_file = "uv.toml"
671
727
  """.strip(),
672
728
  encoding="utf-8",
@@ -687,8 +743,16 @@ uv_config_file = "uv.toml"
687
743
  "install",
688
744
  "--python",
689
745
  "3.12",
746
+ "--default-index",
747
+ "https://default.example/simple",
748
+ "--no-index",
690
749
  "--constraints",
691
750
  str(tmp_path / "constraints.txt"),
751
+ "--no-cache",
752
+ "--refresh",
753
+ "--refresh-package",
754
+ "ruff",
755
+ "--force",
692
756
  "ruff==0.8.0",
693
757
  ]
694
758
  assert calls[0][1]["UV_TOOL_DIR"] == str(result.paths.install_root / "uv-tools")
@@ -344,7 +344,7 @@ wheels = [
344
344
 
345
345
  [[package]]
346
346
  name = "env-module-manager"
347
- version = "0.7.0"
347
+ version = "0.8.0"
348
348
  source = { editable = "." }
349
349
  dependencies = [
350
350
  { name = "click" },
@@ -1,85 +0,0 @@
1
- # Collective Environments
2
-
3
- Collective environments package several tools behind one modulefile. Use them
4
- when a shared workflow needs Python CLIs, Rust binaries, and shell scripts to
5
- appear together from one `module load`.
6
-
7
- Create a TOML manifest:
8
-
9
- ```toml
10
- name = "dev-tools"
11
- version = "2026.05"
12
- description = "Shared developer command-line tools"
13
- prefix = "/prod/tools"
14
- module_root = "/prod/modulefiles"
15
- default = true
16
-
17
- [[tools]]
18
- type = "python"
19
- name = "ruff"
20
- version = "0.8.0"
21
- package = "ruff==0.8.0"
22
- python = "3.12"
23
- indexes = ["https://packages.example/simple"]
24
- find_links = ["/prod/wheels"]
25
- constraints = ["/prod/constraints/runtime.txt"]
26
- uv_config_file = "/prod/config/uv.toml"
27
-
28
- [[tools]]
29
- type = "rust"
30
- name = "ripgrep"
31
- version = "14.1.1"
32
- binary = "./target/release/rg"
33
-
34
- [[tools]]
35
- type = "script"
36
- name = "lab-helper"
37
- version = "1.0.0"
38
- script = "./scripts/lab-helper"
39
- ```
40
-
41
- Deploy the environment:
42
-
43
- ```sh
44
- module-manager deploy-env --file dev-tools.toml
45
- ```
46
-
47
- This writes one shared environment:
48
-
49
- ```text
50
- /prod/tools/dev-tools/2026.05/bin
51
- /prod/modulefiles/dev-tools/2026.05
52
- ```
53
-
54
- The generated modulefile prepends the shared `bin` directory to `PATH`, so all
55
- tools in the manifest are available after:
56
-
57
- ```sh
58
- module use /prod/modulefiles
59
- module load dev-tools
60
- ```
61
-
62
- Relative `binary` and `script` paths are resolved relative to the manifest file.
63
- Relative Python `constraints` paths are resolved the same way.
64
-
65
- ## Preview
66
-
67
- Use `--dry-run` to preview the install, copy, and modulefile actions without
68
- creating files:
69
-
70
- ```sh
71
- module-manager deploy-env --file dev-tools.toml --dry-run
72
- ```
73
-
74
- ## Overrides
75
-
76
- The manifest can define `prefix`, `module_root`, and `default`, but CLI options
77
- can override them:
78
-
79
- ```sh
80
- module-manager deploy-env \
81
- --file dev-tools.toml \
82
- --prefix /scratch/tools \
83
- --module-root /scratch/modulefiles \
84
- --no-default
85
- ```