esys-lpip 0.1.1__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.
- esys_lpip-0.1.1/PKG-INFO +117 -0
- esys_lpip-0.1.1/README.md +101 -0
- esys_lpip-0.1.1/pyproject.toml +48 -0
- esys_lpip-0.1.1/setup.cfg +4 -0
- esys_lpip-0.1.1/src/esys_lpip.egg-info/PKG-INFO +117 -0
- esys_lpip-0.1.1/src/esys_lpip.egg-info/SOURCES.txt +34 -0
- esys_lpip-0.1.1/src/esys_lpip.egg-info/dependency_links.txt +1 -0
- esys_lpip-0.1.1/src/esys_lpip.egg-info/entry_points.txt +2 -0
- esys_lpip-0.1.1/src/esys_lpip.egg-info/requires.txt +8 -0
- esys_lpip-0.1.1/src/esys_lpip.egg-info/top_level.txt +1 -0
- esys_lpip-0.1.1/src/lpip/__init__.py +3 -0
- esys_lpip-0.1.1/src/lpip/cache.py +164 -0
- esys_lpip-0.1.1/src/lpip/catalog.py +223 -0
- esys_lpip-0.1.1/src/lpip/cli.py +311 -0
- esys_lpip-0.1.1/src/lpip/config.py +486 -0
- esys_lpip-0.1.1/src/lpip/errors.py +183 -0
- esys_lpip-0.1.1/src/lpip/gitutils.py +98 -0
- esys_lpip-0.1.1/src/lpip/models.py +66 -0
- esys_lpip-0.1.1/src/lpip/pipwrap.py +42 -0
- esys_lpip-0.1.1/src/lpip/validation.py +97 -0
- esys_lpip-0.1.1/tests/test_cache.py +125 -0
- esys_lpip-0.1.1/tests/test_catalog.py +198 -0
- esys_lpip-0.1.1/tests/test_cli.py +406 -0
- esys_lpip-0.1.1/tests/test_cli_catalog.py +187 -0
- esys_lpip-0.1.1/tests/test_cli_config.py +147 -0
- esys_lpip-0.1.1/tests/test_cli_doctor.py +96 -0
- esys_lpip-0.1.1/tests/test_config.py +268 -0
- esys_lpip-0.1.1/tests/test_errors.py +76 -0
- esys_lpip-0.1.1/tests/test_gitutils.py +48 -0
- esys_lpip-0.1.1/tests/test_install_git_catalog_integration.py +100 -0
- esys_lpip-0.1.1/tests/test_install_integration.py +65 -0
- esys_lpip-0.1.1/tests/test_models.py +62 -0
- esys_lpip-0.1.1/tests/test_pipwrap.py +82 -0
- esys_lpip-0.1.1/tests/test_registry_publish.py +54 -0
- esys_lpip-0.1.1/tests/test_smoke.py +8 -0
- esys_lpip-0.1.1/tests/test_url_normalize.py +66 -0
esys_lpip-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: esys-lpip
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Git-backed, name-oriented front end to pip for internal Python packages
|
|
5
|
+
Author: Project maintainers
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: platformdirs>=4.0
|
|
10
|
+
Requires-Dist: tomlkit>=0.11
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
15
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# lpip
|
|
18
|
+
|
|
19
|
+
Git-backed, name-oriented front end to `pip` for internal Python packages —
|
|
20
|
+
no internal PyPI registry required.
|
|
21
|
+
|
|
22
|
+
Install from PyPI as **`esys-lpip`** (`pip install esys-lpip`); the command
|
|
23
|
+
and import remain **`lpip`**.
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
| Item | Value |
|
|
28
|
+
|------|-------|
|
|
29
|
+
| Workflow | A2C 0.10.0 ([ADR-0000](docs/adr/0000-adopt-a2c-workflow.md)) |
|
|
30
|
+
| Profile | `python-pypi` |
|
|
31
|
+
| Product | Architecture, ADRs, ASRs, and ACs are Accepted; CLI not implemented yet (`src/lpip` is a version stub) |
|
|
32
|
+
|
|
33
|
+
## What this is
|
|
34
|
+
|
|
35
|
+
`lpip` (**local-pip**) resolves a logical package name from a TOML catalog
|
|
36
|
+
(`lpip-sources.toml`), validates configuration and prerequisites, and delegates
|
|
37
|
+
installation to `python -m pip install ...`. The catalog may be a local file
|
|
38
|
+
or a Git-backed checkout in a persistent user cache.
|
|
39
|
+
|
|
40
|
+
It is a small client, not a package manager replacement, registry, or service
|
|
41
|
+
([ADR-0001](docs/adr/0001-pip-delegation.md),
|
|
42
|
+
[ADR-0004](docs/adr/0004-cli-first-no-service.md)).
|
|
43
|
+
|
|
44
|
+
## Non-goals (v1)
|
|
45
|
+
|
|
46
|
+
- Internal package index or hosted registry
|
|
47
|
+
- A dependency resolver other than pip
|
|
48
|
+
- Background daemons, lockfile solving, or workspace orchestration
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
The installable CLI is not available yet. After clone:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m venv .venv
|
|
56
|
+
# Windows: .venv\Scripts\activate
|
|
57
|
+
# Unix: source .venv/bin/activate
|
|
58
|
+
pip install -e ".[dev]"
|
|
59
|
+
pytest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Re-run `scripts/setup-dev` after clone if you need pre-commit and gitlint
|
|
63
|
+
hooks (see [docs/workflow/adoption.md](docs/workflow/adoption.md)).
|
|
64
|
+
|
|
65
|
+
## Intended configuration
|
|
66
|
+
|
|
67
|
+
Local catalog:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[source]
|
|
71
|
+
type = "file"
|
|
72
|
+
path = "~/company/lpip-sources.toml"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Git-backed catalog:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[source]
|
|
79
|
+
type = "git"
|
|
80
|
+
url = "ssh://git.company/dev/lpip-catalog.git"
|
|
81
|
+
revision = "main"
|
|
82
|
+
path = "lpip-sources.toml"
|
|
83
|
+
refresh = "auto"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Example catalog entry:
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[packages.libfoo]
|
|
90
|
+
url = "git+ssh://git.company/libs/libfoo.git"
|
|
91
|
+
revision = "main"
|
|
92
|
+
subdirectory = "python"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Intended commands (not implemented): `lpip install`, `lpip list`,
|
|
96
|
+
`lpip show`, `lpip validate`, `lpip doctor`, `lpip catalog …`,
|
|
97
|
+
`lpip config …` (including `config init`). Details:
|
|
98
|
+
[docs/architecture/cli-and-configuration.md](docs/architecture/cli-and-configuration.md).
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
| Topic | Location |
|
|
103
|
+
|-------|----------|
|
|
104
|
+
| Architecture | [docs/architecture/overview.md](docs/architecture/overview.md) |
|
|
105
|
+
| Artifact chain | [docs/workflow/documentation-structure.md](docs/workflow/documentation-structure.md) |
|
|
106
|
+
| ASRs | [docs/asr/](docs/asr/) |
|
|
107
|
+
| ADRs | [docs/adr/INDEX.md](docs/adr/INDEX.md) |
|
|
108
|
+
| Acceptance criteria | [docs/ac/](docs/ac/) |
|
|
109
|
+
| A2C adoption | [docs/workflow/adoption.md](docs/workflow/adoption.md) |
|
|
110
|
+
| Release | [docs/release-pipeline.md](docs/release-pipeline.md) |
|
|
111
|
+
| Publishing | [docs/package-publishing.md](docs/package-publishing.md) |
|
|
112
|
+
| GitLab CI | [docs/gitlab-ci.md](docs/gitlab-ci.md) |
|
|
113
|
+
| Agent entry | [AGENTS.md](AGENTS.md) |
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT (`pyproject.toml`).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# lpip
|
|
2
|
+
|
|
3
|
+
Git-backed, name-oriented front end to `pip` for internal Python packages —
|
|
4
|
+
no internal PyPI registry required.
|
|
5
|
+
|
|
6
|
+
Install from PyPI as **`esys-lpip`** (`pip install esys-lpip`); the command
|
|
7
|
+
and import remain **`lpip`**.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
| Item | Value |
|
|
12
|
+
|------|-------|
|
|
13
|
+
| Workflow | A2C 0.10.0 ([ADR-0000](docs/adr/0000-adopt-a2c-workflow.md)) |
|
|
14
|
+
| Profile | `python-pypi` |
|
|
15
|
+
| Product | Architecture, ADRs, ASRs, and ACs are Accepted; CLI not implemented yet (`src/lpip` is a version stub) |
|
|
16
|
+
|
|
17
|
+
## What this is
|
|
18
|
+
|
|
19
|
+
`lpip` (**local-pip**) resolves a logical package name from a TOML catalog
|
|
20
|
+
(`lpip-sources.toml`), validates configuration and prerequisites, and delegates
|
|
21
|
+
installation to `python -m pip install ...`. The catalog may be a local file
|
|
22
|
+
or a Git-backed checkout in a persistent user cache.
|
|
23
|
+
|
|
24
|
+
It is a small client, not a package manager replacement, registry, or service
|
|
25
|
+
([ADR-0001](docs/adr/0001-pip-delegation.md),
|
|
26
|
+
[ADR-0004](docs/adr/0004-cli-first-no-service.md)).
|
|
27
|
+
|
|
28
|
+
## Non-goals (v1)
|
|
29
|
+
|
|
30
|
+
- Internal package index or hosted registry
|
|
31
|
+
- A dependency resolver other than pip
|
|
32
|
+
- Background daemons, lockfile solving, or workspace orchestration
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
The installable CLI is not available yet. After clone:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m venv .venv
|
|
40
|
+
# Windows: .venv\Scripts\activate
|
|
41
|
+
# Unix: source .venv/bin/activate
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
pytest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Re-run `scripts/setup-dev` after clone if you need pre-commit and gitlint
|
|
47
|
+
hooks (see [docs/workflow/adoption.md](docs/workflow/adoption.md)).
|
|
48
|
+
|
|
49
|
+
## Intended configuration
|
|
50
|
+
|
|
51
|
+
Local catalog:
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[source]
|
|
55
|
+
type = "file"
|
|
56
|
+
path = "~/company/lpip-sources.toml"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Git-backed catalog:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[source]
|
|
63
|
+
type = "git"
|
|
64
|
+
url = "ssh://git.company/dev/lpip-catalog.git"
|
|
65
|
+
revision = "main"
|
|
66
|
+
path = "lpip-sources.toml"
|
|
67
|
+
refresh = "auto"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Example catalog entry:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[packages.libfoo]
|
|
74
|
+
url = "git+ssh://git.company/libs/libfoo.git"
|
|
75
|
+
revision = "main"
|
|
76
|
+
subdirectory = "python"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Intended commands (not implemented): `lpip install`, `lpip list`,
|
|
80
|
+
`lpip show`, `lpip validate`, `lpip doctor`, `lpip catalog …`,
|
|
81
|
+
`lpip config …` (including `config init`). Details:
|
|
82
|
+
[docs/architecture/cli-and-configuration.md](docs/architecture/cli-and-configuration.md).
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
| Topic | Location |
|
|
87
|
+
|-------|----------|
|
|
88
|
+
| Architecture | [docs/architecture/overview.md](docs/architecture/overview.md) |
|
|
89
|
+
| Artifact chain | [docs/workflow/documentation-structure.md](docs/workflow/documentation-structure.md) |
|
|
90
|
+
| ASRs | [docs/asr/](docs/asr/) |
|
|
91
|
+
| ADRs | [docs/adr/INDEX.md](docs/adr/INDEX.md) |
|
|
92
|
+
| Acceptance criteria | [docs/ac/](docs/ac/) |
|
|
93
|
+
| A2C adoption | [docs/workflow/adoption.md](docs/workflow/adoption.md) |
|
|
94
|
+
| Release | [docs/release-pipeline.md](docs/release-pipeline.md) |
|
|
95
|
+
| Publishing | [docs/package-publishing.md](docs/package-publishing.md) |
|
|
96
|
+
| GitLab CI | [docs/gitlab-ci.md](docs/gitlab-ci.md) |
|
|
97
|
+
| Agent entry | [AGENTS.md](AGENTS.md) |
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT (`pyproject.toml`).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "esys-lpip"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Git-backed, name-oriented front end to pip for internal Python packages"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Project maintainers" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"platformdirs>=4.0",
|
|
15
|
+
"tomlkit>=0.11",
|
|
16
|
+
"typer>=0.12",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
"ruff>=0.8",
|
|
23
|
+
"pre-commit>=3.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = { "" = "src" }
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
lpip = "lpip.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
pythonpath = ["src", "tests"]
|
|
38
|
+
markers = [
|
|
39
|
+
"integration: live venv + pip (network/build isolation)",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py311"
|
|
45
|
+
src = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: esys-lpip
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Git-backed, name-oriented front end to pip for internal Python packages
|
|
5
|
+
Author: Project maintainers
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: platformdirs>=4.0
|
|
10
|
+
Requires-Dist: tomlkit>=0.11
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
15
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# lpip
|
|
18
|
+
|
|
19
|
+
Git-backed, name-oriented front end to `pip` for internal Python packages —
|
|
20
|
+
no internal PyPI registry required.
|
|
21
|
+
|
|
22
|
+
Install from PyPI as **`esys-lpip`** (`pip install esys-lpip`); the command
|
|
23
|
+
and import remain **`lpip`**.
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
| Item | Value |
|
|
28
|
+
|------|-------|
|
|
29
|
+
| Workflow | A2C 0.10.0 ([ADR-0000](docs/adr/0000-adopt-a2c-workflow.md)) |
|
|
30
|
+
| Profile | `python-pypi` |
|
|
31
|
+
| Product | Architecture, ADRs, ASRs, and ACs are Accepted; CLI not implemented yet (`src/lpip` is a version stub) |
|
|
32
|
+
|
|
33
|
+
## What this is
|
|
34
|
+
|
|
35
|
+
`lpip` (**local-pip**) resolves a logical package name from a TOML catalog
|
|
36
|
+
(`lpip-sources.toml`), validates configuration and prerequisites, and delegates
|
|
37
|
+
installation to `python -m pip install ...`. The catalog may be a local file
|
|
38
|
+
or a Git-backed checkout in a persistent user cache.
|
|
39
|
+
|
|
40
|
+
It is a small client, not a package manager replacement, registry, or service
|
|
41
|
+
([ADR-0001](docs/adr/0001-pip-delegation.md),
|
|
42
|
+
[ADR-0004](docs/adr/0004-cli-first-no-service.md)).
|
|
43
|
+
|
|
44
|
+
## Non-goals (v1)
|
|
45
|
+
|
|
46
|
+
- Internal package index or hosted registry
|
|
47
|
+
- A dependency resolver other than pip
|
|
48
|
+
- Background daemons, lockfile solving, or workspace orchestration
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
The installable CLI is not available yet. After clone:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m venv .venv
|
|
56
|
+
# Windows: .venv\Scripts\activate
|
|
57
|
+
# Unix: source .venv/bin/activate
|
|
58
|
+
pip install -e ".[dev]"
|
|
59
|
+
pytest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Re-run `scripts/setup-dev` after clone if you need pre-commit and gitlint
|
|
63
|
+
hooks (see [docs/workflow/adoption.md](docs/workflow/adoption.md)).
|
|
64
|
+
|
|
65
|
+
## Intended configuration
|
|
66
|
+
|
|
67
|
+
Local catalog:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[source]
|
|
71
|
+
type = "file"
|
|
72
|
+
path = "~/company/lpip-sources.toml"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Git-backed catalog:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[source]
|
|
79
|
+
type = "git"
|
|
80
|
+
url = "ssh://git.company/dev/lpip-catalog.git"
|
|
81
|
+
revision = "main"
|
|
82
|
+
path = "lpip-sources.toml"
|
|
83
|
+
refresh = "auto"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Example catalog entry:
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[packages.libfoo]
|
|
90
|
+
url = "git+ssh://git.company/libs/libfoo.git"
|
|
91
|
+
revision = "main"
|
|
92
|
+
subdirectory = "python"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Intended commands (not implemented): `lpip install`, `lpip list`,
|
|
96
|
+
`lpip show`, `lpip validate`, `lpip doctor`, `lpip catalog …`,
|
|
97
|
+
`lpip config …` (including `config init`). Details:
|
|
98
|
+
[docs/architecture/cli-and-configuration.md](docs/architecture/cli-and-configuration.md).
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
| Topic | Location |
|
|
103
|
+
|-------|----------|
|
|
104
|
+
| Architecture | [docs/architecture/overview.md](docs/architecture/overview.md) |
|
|
105
|
+
| Artifact chain | [docs/workflow/documentation-structure.md](docs/workflow/documentation-structure.md) |
|
|
106
|
+
| ASRs | [docs/asr/](docs/asr/) |
|
|
107
|
+
| ADRs | [docs/adr/INDEX.md](docs/adr/INDEX.md) |
|
|
108
|
+
| Acceptance criteria | [docs/ac/](docs/ac/) |
|
|
109
|
+
| A2C adoption | [docs/workflow/adoption.md](docs/workflow/adoption.md) |
|
|
110
|
+
| Release | [docs/release-pipeline.md](docs/release-pipeline.md) |
|
|
111
|
+
| Publishing | [docs/package-publishing.md](docs/package-publishing.md) |
|
|
112
|
+
| GitLab CI | [docs/gitlab-ci.md](docs/gitlab-ci.md) |
|
|
113
|
+
| Agent entry | [AGENTS.md](AGENTS.md) |
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT (`pyproject.toml`).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/esys_lpip.egg-info/PKG-INFO
|
|
4
|
+
src/esys_lpip.egg-info/SOURCES.txt
|
|
5
|
+
src/esys_lpip.egg-info/dependency_links.txt
|
|
6
|
+
src/esys_lpip.egg-info/entry_points.txt
|
|
7
|
+
src/esys_lpip.egg-info/requires.txt
|
|
8
|
+
src/esys_lpip.egg-info/top_level.txt
|
|
9
|
+
src/lpip/__init__.py
|
|
10
|
+
src/lpip/cache.py
|
|
11
|
+
src/lpip/catalog.py
|
|
12
|
+
src/lpip/cli.py
|
|
13
|
+
src/lpip/config.py
|
|
14
|
+
src/lpip/errors.py
|
|
15
|
+
src/lpip/gitutils.py
|
|
16
|
+
src/lpip/models.py
|
|
17
|
+
src/lpip/pipwrap.py
|
|
18
|
+
src/lpip/validation.py
|
|
19
|
+
tests/test_cache.py
|
|
20
|
+
tests/test_catalog.py
|
|
21
|
+
tests/test_cli.py
|
|
22
|
+
tests/test_cli_catalog.py
|
|
23
|
+
tests/test_cli_config.py
|
|
24
|
+
tests/test_cli_doctor.py
|
|
25
|
+
tests/test_config.py
|
|
26
|
+
tests/test_errors.py
|
|
27
|
+
tests/test_gitutils.py
|
|
28
|
+
tests/test_install_git_catalog_integration.py
|
|
29
|
+
tests/test_install_integration.py
|
|
30
|
+
tests/test_models.py
|
|
31
|
+
tests/test_pipwrap.py
|
|
32
|
+
tests/test_registry_publish.py
|
|
33
|
+
tests/test_smoke.py
|
|
34
|
+
tests/test_url_normalize.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lpip
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Persistent Git catalog cache and auto-refresh TTL (ADR-0003, ADR-0012)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import UTC, datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from lpip.config import default_user_cache_dir
|
|
16
|
+
from lpip.gitutils import git_checkout, git_clone, git_fetch
|
|
17
|
+
from lpip.models import RefreshMode
|
|
18
|
+
|
|
19
|
+
AUTO_REFRESH_TTL = timedelta(hours=24)
|
|
20
|
+
_METADATA_NAME = "metadata.json"
|
|
21
|
+
_LAST_SUCCESS_KEY = "last_success"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CatalogCachePaths:
|
|
26
|
+
root: Path
|
|
27
|
+
repo: Path
|
|
28
|
+
metadata: Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def catalog_identity_hash(url: str, revision: str | None) -> str:
|
|
32
|
+
payload = f"{url}\n{revision or ''}".encode()
|
|
33
|
+
return hashlib.sha256(payload).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def catalog_cache_paths(
|
|
37
|
+
url: str,
|
|
38
|
+
revision: str | None,
|
|
39
|
+
*,
|
|
40
|
+
cache_home: Path | None = None,
|
|
41
|
+
) -> CatalogCachePaths:
|
|
42
|
+
home = cache_home if cache_home is not None else default_user_cache_dir()
|
|
43
|
+
root = home / "catalogs" / catalog_identity_hash(url, revision)
|
|
44
|
+
return CatalogCachePaths(root=root, repo=root / "repo", metadata=root / _METADATA_NAME)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_last_success(paths: CatalogCachePaths) -> datetime | None:
|
|
48
|
+
if not paths.metadata.is_file():
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(paths.metadata.read_text(encoding="utf-8"))
|
|
52
|
+
except (OSError, json.JSONDecodeError):
|
|
53
|
+
return None
|
|
54
|
+
raw = data.get(_LAST_SUCCESS_KEY)
|
|
55
|
+
if not isinstance(raw, str) or not raw:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
parsed = datetime.fromisoformat(raw)
|
|
59
|
+
except ValueError:
|
|
60
|
+
return None
|
|
61
|
+
if parsed.tzinfo is None:
|
|
62
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
63
|
+
return parsed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def write_last_success(paths: CatalogCachePaths, when: datetime | None = None) -> None:
|
|
67
|
+
"""Record last successful clone/fetch. Call only after Git succeeds."""
|
|
68
|
+
stamp = when or datetime.now(UTC)
|
|
69
|
+
if stamp.tzinfo is None:
|
|
70
|
+
stamp = stamp.replace(tzinfo=UTC)
|
|
71
|
+
payload: dict[str, object] = {}
|
|
72
|
+
if paths.metadata.is_file():
|
|
73
|
+
try:
|
|
74
|
+
loaded = json.loads(paths.metadata.read_text(encoding="utf-8"))
|
|
75
|
+
if isinstance(loaded, dict):
|
|
76
|
+
payload = loaded
|
|
77
|
+
except (OSError, json.JSONDecodeError):
|
|
78
|
+
payload = {}
|
|
79
|
+
payload[_LAST_SUCCESS_KEY] = stamp.isoformat()
|
|
80
|
+
paths.root.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
paths.metadata.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def needs_auto_refresh(
|
|
85
|
+
paths: CatalogCachePaths,
|
|
86
|
+
*,
|
|
87
|
+
now: datetime | None = None,
|
|
88
|
+
) -> bool:
|
|
89
|
+
"""True if timestamp is missing or older than 24 hours (ADR-0012)."""
|
|
90
|
+
last = read_last_success(paths)
|
|
91
|
+
if last is None:
|
|
92
|
+
return True
|
|
93
|
+
clock = now or datetime.now(UTC)
|
|
94
|
+
if clock.tzinfo is None:
|
|
95
|
+
clock = clock.replace(tzinfo=UTC)
|
|
96
|
+
return clock - last >= AUTO_REFRESH_TTL
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def should_fetch(
|
|
100
|
+
paths: CatalogCachePaths,
|
|
101
|
+
refresh: RefreshMode,
|
|
102
|
+
*,
|
|
103
|
+
force: bool = False,
|
|
104
|
+
now: datetime | None = None,
|
|
105
|
+
) -> bool:
|
|
106
|
+
"""Whether to fetch an existing checkout. Absent cache is always cloned."""
|
|
107
|
+
if force or refresh is RefreshMode.ALWAYS:
|
|
108
|
+
return True
|
|
109
|
+
if refresh is RefreshMode.NEVER:
|
|
110
|
+
return False
|
|
111
|
+
return needs_auto_refresh(paths, now=now)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _repo_ready(repo: Path) -> bool:
|
|
115
|
+
git_dir = repo / ".git"
|
|
116
|
+
return git_dir.is_dir() or git_dir.is_file()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ensure_catalog_checkout(
|
|
120
|
+
url: str,
|
|
121
|
+
revision: str | None,
|
|
122
|
+
*,
|
|
123
|
+
cache_home: Path | None = None,
|
|
124
|
+
refresh: RefreshMode = RefreshMode.AUTO,
|
|
125
|
+
force_refresh: bool = False,
|
|
126
|
+
now: datetime | None = None,
|
|
127
|
+
environ: Mapping[str, str] | None = None,
|
|
128
|
+
) -> CatalogCachePaths:
|
|
129
|
+
"""Clone if missing; fetch per refresh policy; stamp only on Git success."""
|
|
130
|
+
paths = catalog_cache_paths(url, revision, cache_home=cache_home)
|
|
131
|
+
if not _repo_ready(paths.repo):
|
|
132
|
+
git_clone(url, paths.repo, environ=environ)
|
|
133
|
+
write_last_success(paths, now)
|
|
134
|
+
if revision:
|
|
135
|
+
git_checkout(paths.repo, revision, environ=environ)
|
|
136
|
+
return paths
|
|
137
|
+
|
|
138
|
+
if should_fetch(paths, refresh, force=force_refresh, now=now):
|
|
139
|
+
git_fetch(paths.repo, environ=environ)
|
|
140
|
+
write_last_success(paths, now)
|
|
141
|
+
if revision:
|
|
142
|
+
git_checkout(paths.repo, revision, environ=environ)
|
|
143
|
+
return paths
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _rmtree(path: Path) -> None:
|
|
147
|
+
def _onerror(func: object, name: str, _exc: object) -> None:
|
|
148
|
+
os.chmod(name, stat.S_IWRITE)
|
|
149
|
+
func(name) # type: ignore[operator]
|
|
150
|
+
|
|
151
|
+
shutil.rmtree(path, onerror=_onerror)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def purge_catalog_cache(
|
|
155
|
+
url: str,
|
|
156
|
+
revision: str | None,
|
|
157
|
+
*,
|
|
158
|
+
cache_home: Path | None = None,
|
|
159
|
+
) -> CatalogCachePaths:
|
|
160
|
+
"""Remove the cache directory for this catalog identity."""
|
|
161
|
+
paths = catalog_cache_paths(url, revision, cache_home=cache_home)
|
|
162
|
+
if paths.root.exists():
|
|
163
|
+
_rmtree(paths.root)
|
|
164
|
+
return paths
|