libyear-multi 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.
- libyear_multi-0.1.0/LICENSE +21 -0
- libyear_multi-0.1.0/PKG-INFO +183 -0
- libyear_multi-0.1.0/README.md +167 -0
- libyear_multi-0.1.0/libyear_multi/__init__.py +16 -0
- libyear_multi-0.1.0/libyear_multi/adapters/__init__.py +14 -0
- libyear_multi-0.1.0/libyear_multi/adapters/cargo.py +51 -0
- libyear_multi-0.1.0/libyear_multi/adapters/npm.py +50 -0
- libyear_multi-0.1.0/libyear_multi/adapters/pypi.py +50 -0
- libyear_multi-0.1.0/libyear_multi/adapters/rubygems.py +64 -0
- libyear_multi-0.1.0/libyear_multi/cache.py +112 -0
- libyear_multi-0.1.0/libyear_multi/cli.py +72 -0
- libyear_multi-0.1.0/libyear_multi/core.py +59 -0
- libyear_multi-0.1.0/libyear_multi/scanner.py +77 -0
- libyear_multi-0.1.0/libyear_multi/scoring.py +59 -0
- libyear_multi-0.1.0/libyear_multi.egg-info/PKG-INFO +183 -0
- libyear_multi-0.1.0/libyear_multi.egg-info/SOURCES.txt +22 -0
- libyear_multi-0.1.0/libyear_multi.egg-info/dependency_links.txt +1 -0
- libyear_multi-0.1.0/libyear_multi.egg-info/entry_points.txt +2 -0
- libyear_multi-0.1.0/libyear_multi.egg-info/requires.txt +3 -0
- libyear_multi-0.1.0/libyear_multi.egg-info/top_level.txt +1 -0
- libyear_multi-0.1.0/pyproject.toml +30 -0
- libyear_multi-0.1.0/setup.cfg +4 -0
- libyear_multi-0.1.0/tests/test_adapters_parsing.py +62 -0
- libyear_multi-0.1.0/tests/test_core.py +53 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shreyas Dhakal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: libyear-multi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Language-agnostic libyear: dependency staleness scoring across PyPI, npm, crates.io, RubyGems and more.
|
|
5
|
+
Author: Shreyas Dhakal
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# libyear-multi
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
`libyear-multi` measures dependency staleness across the package managers used
|
|
22
|
+
by a project. It detects supported manifests, looks up release dates, and
|
|
23
|
+
reports a single score that can be used to prioritise maintenance work.
|
|
24
|
+
|
|
25
|
+
The [libyear](https://libyear.com/) metric measures dependency age in calendar
|
|
26
|
+
time rather than semantic-version distance. A package released three years ago
|
|
27
|
+
whose latest release shipped yesterday represents approximately three libyears
|
|
28
|
+
of staleness.
|
|
29
|
+
|
|
30
|
+
## Highlights
|
|
31
|
+
|
|
32
|
+
- Scans multiple ecosystems in one repository, including monorepos.
|
|
33
|
+
- Reports total, average, median, and maximum libyears.
|
|
34
|
+
- Classifies results into low, moderate, high, and severe risk bands.
|
|
35
|
+
- Provides both human-readable and JSON output.
|
|
36
|
+
- Caches registry responses locally to reduce repeated network requests.
|
|
37
|
+
|
|
38
|
+
## Supported Ecosystems
|
|
39
|
+
|
|
40
|
+
| Ecosystem | Manifest file | Registry |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| Python | `requirements.txt` with pinned `==` versions | PyPI |
|
|
43
|
+
| Node.js | `package.json` | npm registry |
|
|
44
|
+
| Rust | `Cargo.lock` | crates.io |
|
|
45
|
+
| Ruby | `Gemfile.lock` | RubyGems |
|
|
46
|
+
|
|
47
|
+
Additional adapters are welcome. See [Adding an ecosystem](#adding-an-ecosystem).
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python -m pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The package is not published to PyPI yet. Install it from a clone or use the
|
|
56
|
+
`libyear_multi/` package directly.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Command line
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m libyear_multi.cli /path/to/project
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Example output:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
14 dependencies scanned: 22.4 total libyears of staleness (avg 1.6 yrs/package, 21% severely outdated).
|
|
70
|
+
Risk band: high
|
|
71
|
+
|
|
72
|
+
Most outdated dependencies:
|
|
73
|
+
[npm] left-pad: 1.1.0 -> 1.3.0 (4.2 libyears)
|
|
74
|
+
[pypi] requests: 2.20.0 -> 2.32.3 (3.8 libyears)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use JSON output for scripts and CI integrations:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m libyear_multi.cli /path/to/project --json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The number of concurrent registry lookups can be configured with
|
|
84
|
+
`--max-workers`.
|
|
85
|
+
|
|
86
|
+
### Python API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from libyear_multi import LibyearScanner, score_dependency_age, summary_line
|
|
90
|
+
|
|
91
|
+
scanner = LibyearScanner()
|
|
92
|
+
dependencies = scanner.scan("/path/to/project")
|
|
93
|
+
|
|
94
|
+
score = score_dependency_age(dependencies)
|
|
95
|
+
print(summary_line(score))
|
|
96
|
+
print(score.to_risk_band())
|
|
97
|
+
|
|
98
|
+
for dependency in dependencies:
|
|
99
|
+
print(dependency.name, dependency.ecosystem, dependency.libyears)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## How It Works
|
|
103
|
+
|
|
104
|
+
1. Each adapter checks whether its manifest exists in the target directory.
|
|
105
|
+
2. Detected adapters parse the manifest into package and installed-version pairs.
|
|
106
|
+
3. Registry APIs provide the latest version and release dates.
|
|
107
|
+
4. Staleness is calculated as `(latest_date - installed_date).days / 365.25`.
|
|
108
|
+
5. Individual results are aggregated into a project score.
|
|
109
|
+
|
|
110
|
+
Release dates are cached permanently in
|
|
111
|
+
`~/.cache/libyear-multi/cache.sqlite3`. Latest-version lookups expire after 24
|
|
112
|
+
hours.
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
Clone the repository and install the development dependencies:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git clone https://github.com/shreyasdhakal/libyear-multi.git
|
|
120
|
+
cd libyear-multi
|
|
121
|
+
python -m pip install -e ".[dev]"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Run the test suite:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
python -m pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Before opening a pull request, make sure tests pass and that changes are
|
|
131
|
+
covered by tests where practical. Network-dependent registry calls are not part
|
|
132
|
+
of the unit test suite.
|
|
133
|
+
|
|
134
|
+
## Adding an Ecosystem
|
|
135
|
+
|
|
136
|
+
Subclass `EcosystemAdapter` in `libyear_multi/core.py` and implement its four
|
|
137
|
+
abstract methods:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from datetime import datetime
|
|
141
|
+
|
|
142
|
+
from libyear_multi.core import EcosystemAdapter
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class MyAdapter(EcosystemAdapter):
|
|
146
|
+
name = "my_ecosystem"
|
|
147
|
+
|
|
148
|
+
def detect(self, project_path: str) -> bool: ...
|
|
149
|
+
def list_dependencies(self, project_path: str) -> list[tuple[str, str]]: ...
|
|
150
|
+
def get_latest_version(self, name: str) -> str | None: ...
|
|
151
|
+
def get_release_date(self, name: str, version: str) -> datetime | None: ...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Register the adapter in `libyear_multi/adapters/__init__.py` and add parsing
|
|
155
|
+
tests in `tests/`. Good future candidates include Go, Maven, Composer, and
|
|
156
|
+
NuGet.
|
|
157
|
+
|
|
158
|
+
## Known Limitations
|
|
159
|
+
|
|
160
|
+
- Python adapters currently expect pinned `==` versions.
|
|
161
|
+
- Rust and Ruby adapters read lockfiles, while npm currently reads `package.json`.
|
|
162
|
+
- Non-registry npm specifications such as Git URLs and workspace references are skipped.
|
|
163
|
+
- Transitive dependencies are only included when they appear in a parsed lockfile.
|
|
164
|
+
- Large repositories may encounter registry rate limits. The default worker limit is eight and responses are cached.
|
|
165
|
+
- The metric uses registry release dates and does not measure repository activity or abandonment.
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before
|
|
170
|
+
submitting a change. Bug reports, documentation improvements, new adapters, and
|
|
171
|
+
focused fixes are all useful.
|
|
172
|
+
|
|
173
|
+
## Security
|
|
174
|
+
|
|
175
|
+
Please report suspected vulnerabilities privately by following the instructions
|
|
176
|
+
in [SECURITY.md](SECURITY.md). Do not disclose exploitable details in a public
|
|
177
|
+
issue.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
182
|
+
|
|
183
|
+
Copyright (c) 2026 Shreyas Dhakal.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# libyear-multi
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
`libyear-multi` measures dependency staleness across the package managers used
|
|
6
|
+
by a project. It detects supported manifests, looks up release dates, and
|
|
7
|
+
reports a single score that can be used to prioritise maintenance work.
|
|
8
|
+
|
|
9
|
+
The [libyear](https://libyear.com/) metric measures dependency age in calendar
|
|
10
|
+
time rather than semantic-version distance. A package released three years ago
|
|
11
|
+
whose latest release shipped yesterday represents approximately three libyears
|
|
12
|
+
of staleness.
|
|
13
|
+
|
|
14
|
+
## Highlights
|
|
15
|
+
|
|
16
|
+
- Scans multiple ecosystems in one repository, including monorepos.
|
|
17
|
+
- Reports total, average, median, and maximum libyears.
|
|
18
|
+
- Classifies results into low, moderate, high, and severe risk bands.
|
|
19
|
+
- Provides both human-readable and JSON output.
|
|
20
|
+
- Caches registry responses locally to reduce repeated network requests.
|
|
21
|
+
|
|
22
|
+
## Supported Ecosystems
|
|
23
|
+
|
|
24
|
+
| Ecosystem | Manifest file | Registry |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Python | `requirements.txt` with pinned `==` versions | PyPI |
|
|
27
|
+
| Node.js | `package.json` | npm registry |
|
|
28
|
+
| Rust | `Cargo.lock` | crates.io |
|
|
29
|
+
| Ruby | `Gemfile.lock` | RubyGems |
|
|
30
|
+
|
|
31
|
+
Additional adapters are welcome. See [Adding an ecosystem](#adding-an-ecosystem).
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The package is not published to PyPI yet. Install it from a clone or use the
|
|
40
|
+
`libyear_multi/` package directly.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Command line
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m libyear_multi.cli /path/to/project
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Example output:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
14 dependencies scanned: 22.4 total libyears of staleness (avg 1.6 yrs/package, 21% severely outdated).
|
|
54
|
+
Risk band: high
|
|
55
|
+
|
|
56
|
+
Most outdated dependencies:
|
|
57
|
+
[npm] left-pad: 1.1.0 -> 1.3.0 (4.2 libyears)
|
|
58
|
+
[pypi] requests: 2.20.0 -> 2.32.3 (3.8 libyears)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use JSON output for scripts and CI integrations:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m libyear_multi.cli /path/to/project --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The number of concurrent registry lookups can be configured with
|
|
68
|
+
`--max-workers`.
|
|
69
|
+
|
|
70
|
+
### Python API
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from libyear_multi import LibyearScanner, score_dependency_age, summary_line
|
|
74
|
+
|
|
75
|
+
scanner = LibyearScanner()
|
|
76
|
+
dependencies = scanner.scan("/path/to/project")
|
|
77
|
+
|
|
78
|
+
score = score_dependency_age(dependencies)
|
|
79
|
+
print(summary_line(score))
|
|
80
|
+
print(score.to_risk_band())
|
|
81
|
+
|
|
82
|
+
for dependency in dependencies:
|
|
83
|
+
print(dependency.name, dependency.ecosystem, dependency.libyears)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## How It Works
|
|
87
|
+
|
|
88
|
+
1. Each adapter checks whether its manifest exists in the target directory.
|
|
89
|
+
2. Detected adapters parse the manifest into package and installed-version pairs.
|
|
90
|
+
3. Registry APIs provide the latest version and release dates.
|
|
91
|
+
4. Staleness is calculated as `(latest_date - installed_date).days / 365.25`.
|
|
92
|
+
5. Individual results are aggregated into a project score.
|
|
93
|
+
|
|
94
|
+
Release dates are cached permanently in
|
|
95
|
+
`~/.cache/libyear-multi/cache.sqlite3`. Latest-version lookups expire after 24
|
|
96
|
+
hours.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
Clone the repository and install the development dependencies:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/shreyasdhakal/libyear-multi.git
|
|
104
|
+
cd libyear-multi
|
|
105
|
+
python -m pip install -e ".[dev]"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Run the test suite:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python -m pytest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Before opening a pull request, make sure tests pass and that changes are
|
|
115
|
+
covered by tests where practical. Network-dependent registry calls are not part
|
|
116
|
+
of the unit test suite.
|
|
117
|
+
|
|
118
|
+
## Adding an Ecosystem
|
|
119
|
+
|
|
120
|
+
Subclass `EcosystemAdapter` in `libyear_multi/core.py` and implement its four
|
|
121
|
+
abstract methods:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from datetime import datetime
|
|
125
|
+
|
|
126
|
+
from libyear_multi.core import EcosystemAdapter
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MyAdapter(EcosystemAdapter):
|
|
130
|
+
name = "my_ecosystem"
|
|
131
|
+
|
|
132
|
+
def detect(self, project_path: str) -> bool: ...
|
|
133
|
+
def list_dependencies(self, project_path: str) -> list[tuple[str, str]]: ...
|
|
134
|
+
def get_latest_version(self, name: str) -> str | None: ...
|
|
135
|
+
def get_release_date(self, name: str, version: str) -> datetime | None: ...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Register the adapter in `libyear_multi/adapters/__init__.py` and add parsing
|
|
139
|
+
tests in `tests/`. Good future candidates include Go, Maven, Composer, and
|
|
140
|
+
NuGet.
|
|
141
|
+
|
|
142
|
+
## Known Limitations
|
|
143
|
+
|
|
144
|
+
- Python adapters currently expect pinned `==` versions.
|
|
145
|
+
- Rust and Ruby adapters read lockfiles, while npm currently reads `package.json`.
|
|
146
|
+
- Non-registry npm specifications such as Git URLs and workspace references are skipped.
|
|
147
|
+
- Transitive dependencies are only included when they appear in a parsed lockfile.
|
|
148
|
+
- Large repositories may encounter registry rate limits. The default worker limit is eight and responses are cached.
|
|
149
|
+
- The metric uses registry release dates and does not measure repository activity or abandonment.
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before
|
|
154
|
+
submitting a change. Bug reports, documentation improvements, new adapters, and
|
|
155
|
+
focused fixes are all useful.
|
|
156
|
+
|
|
157
|
+
## Security
|
|
158
|
+
|
|
159
|
+
Please report suspected vulnerabilities privately by following the instructions
|
|
160
|
+
in [SECURITY.md](SECURITY.md). Do not disclose exploitable details in a public
|
|
161
|
+
issue.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
166
|
+
|
|
167
|
+
Copyright (c) 2026 Shreyas Dhakal.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .core import Dependency, EcosystemAdapter
|
|
2
|
+
from .scanner import LibyearScanner
|
|
3
|
+
from .scoring import DependencyAgeScore, score_dependency_age, summary_line
|
|
4
|
+
from .cache import Cache
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Dependency",
|
|
10
|
+
"EcosystemAdapter",
|
|
11
|
+
"LibyearScanner",
|
|
12
|
+
"DependencyAgeScore",
|
|
13
|
+
"score_dependency_age",
|
|
14
|
+
"summary_line",
|
|
15
|
+
"Cache",
|
|
16
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .pypi import PyPIAdapter
|
|
2
|
+
from .npm import NpmAdapter
|
|
3
|
+
from .cargo import CargoAdapter
|
|
4
|
+
from .rubygems import RubyGemsAdapter
|
|
5
|
+
|
|
6
|
+
ALL_ADAPTERS = [PyPIAdapter, NpmAdapter, CargoAdapter, RubyGemsAdapter]
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"PyPIAdapter",
|
|
10
|
+
"NpmAdapter",
|
|
11
|
+
"CargoAdapter",
|
|
12
|
+
"RubyGemsAdapter",
|
|
13
|
+
"ALL_ADAPTERS",
|
|
14
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""crates.io adapter for Rust Cargo.lock files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import urllib.request
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from ..core import EcosystemAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CargoAdapter(EcosystemAdapter):
|
|
13
|
+
name = "cargo"
|
|
14
|
+
|
|
15
|
+
def detect(self, project_path: str) -> bool:
|
|
16
|
+
return os.path.exists(os.path.join(project_path, "Cargo.lock"))
|
|
17
|
+
|
|
18
|
+
def list_dependencies(self, project_path: str) -> list[tuple[str, str]]:
|
|
19
|
+
path = os.path.join(project_path, "Cargo.lock")
|
|
20
|
+
deps = []
|
|
21
|
+
with open(path) as f:
|
|
22
|
+
content = f.read()
|
|
23
|
+
# Parse package blocks directly so the core package has no TOML runtime
|
|
24
|
+
# dependency. This is sufficient for the fields needed by the scanner.
|
|
25
|
+
for block in content.split("[[package]]")[1:]:
|
|
26
|
+
name_match = re.search(r'name\s*=\s*"([^"]+)"', block)
|
|
27
|
+
version_match = re.search(r'version\s*=\s*"([^"]+)"', block)
|
|
28
|
+
if name_match and version_match:
|
|
29
|
+
deps.append((name_match.group(1), version_match.group(1)))
|
|
30
|
+
return deps
|
|
31
|
+
|
|
32
|
+
def get_latest_version(self, name: str) -> str | None:
|
|
33
|
+
try:
|
|
34
|
+
url = f"https://crates.io/api/v1/crates/{name}"
|
|
35
|
+
req = urllib.request.Request(url, headers={"User-Agent": "libyear-multi"})
|
|
36
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
37
|
+
data = json.load(r)
|
|
38
|
+
return data["crate"]["max_stable_version"]
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def get_release_date(self, name: str, version: str) -> datetime | None:
|
|
43
|
+
try:
|
|
44
|
+
url = f"https://crates.io/api/v1/crates/{name}/{version}"
|
|
45
|
+
req = urllib.request.Request(url, headers={"User-Agent": "libyear-multi"})
|
|
46
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
47
|
+
data = json.load(r)
|
|
48
|
+
ts = data["version"]["created_at"]
|
|
49
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""npm adapter for Node.js package.json files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import urllib.request
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ..core import EcosystemAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NpmAdapter(EcosystemAdapter):
|
|
12
|
+
name = "npm"
|
|
13
|
+
|
|
14
|
+
def detect(self, project_path: str) -> bool:
|
|
15
|
+
return os.path.exists(os.path.join(project_path, "package.json"))
|
|
16
|
+
|
|
17
|
+
def list_dependencies(self, project_path: str) -> list[tuple[str, str]]:
|
|
18
|
+
path = os.path.join(project_path, "package.json")
|
|
19
|
+
with open(path) as f:
|
|
20
|
+
pkg = json.load(f)
|
|
21
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
22
|
+
cleaned = []
|
|
23
|
+
for name, version in deps.items():
|
|
24
|
+
v = version.lstrip("^~=v ").strip()
|
|
25
|
+
# Registry metadata cannot resolve Git, file, or workspace specs.
|
|
26
|
+
if any(c in v for c in ["/", ":", "*", "workspace"]):
|
|
27
|
+
continue
|
|
28
|
+
cleaned.append((name, v))
|
|
29
|
+
return cleaned
|
|
30
|
+
|
|
31
|
+
def get_latest_version(self, name: str) -> str | None:
|
|
32
|
+
try:
|
|
33
|
+
url = f"https://registry.npmjs.org/{name}"
|
|
34
|
+
with urllib.request.urlopen(url, timeout=10) as r:
|
|
35
|
+
data = json.load(r)
|
|
36
|
+
return data.get("dist-tags", {}).get("latest")
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def get_release_date(self, name: str, version: str) -> datetime | None:
|
|
41
|
+
try:
|
|
42
|
+
url = f"https://registry.npmjs.org/{name}"
|
|
43
|
+
with urllib.request.urlopen(url, timeout=10) as r:
|
|
44
|
+
data = json.load(r)
|
|
45
|
+
ts = data.get("time", {}).get(version)
|
|
46
|
+
if not ts:
|
|
47
|
+
return None
|
|
48
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""PyPI adapter for Python requirements.txt files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import urllib.request
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from ..core import EcosystemAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PyPIAdapter(EcosystemAdapter):
|
|
13
|
+
name = "pypi"
|
|
14
|
+
|
|
15
|
+
def detect(self, project_path: str) -> bool:
|
|
16
|
+
return os.path.exists(os.path.join(project_path, "requirements.txt"))
|
|
17
|
+
|
|
18
|
+
def list_dependencies(self, project_path: str) -> list[tuple[str, str]]:
|
|
19
|
+
deps = []
|
|
20
|
+
path = os.path.join(project_path, "requirements.txt")
|
|
21
|
+
with open(path) as f:
|
|
22
|
+
for line in f:
|
|
23
|
+
line = line.strip()
|
|
24
|
+
if not line or line.startswith("#"):
|
|
25
|
+
continue
|
|
26
|
+
match = re.match(r"^([A-Za-z0-9_.\-]+)\s*==\s*([A-Za-z0-9_.\-]+)", line)
|
|
27
|
+
if match:
|
|
28
|
+
deps.append((match.group(1), match.group(2)))
|
|
29
|
+
return deps
|
|
30
|
+
|
|
31
|
+
def get_latest_version(self, name: str) -> str | None:
|
|
32
|
+
try:
|
|
33
|
+
with urllib.request.urlopen(f"https://pypi.org/pypi/{name}/json", timeout=10) as r:
|
|
34
|
+
data = json.load(r)
|
|
35
|
+
return data["info"]["version"]
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def get_release_date(self, name: str, version: str) -> datetime | None:
|
|
40
|
+
try:
|
|
41
|
+
url = f"https://pypi.org/pypi/{name}/{version}/json"
|
|
42
|
+
with urllib.request.urlopen(url, timeout=10) as r:
|
|
43
|
+
data = json.load(r)
|
|
44
|
+
urls = data.get("urls") or []
|
|
45
|
+
if not urls:
|
|
46
|
+
return None
|
|
47
|
+
ts = urls[0]["upload_time_iso_8601"].replace("Z", "+00:00")
|
|
48
|
+
return datetime.fromisoformat(ts)
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""RubyGems adapter for Ruby Gemfile.lock files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import urllib.request
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from ..core import EcosystemAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RubyGemsAdapter(EcosystemAdapter):
|
|
13
|
+
name = "rubygems"
|
|
14
|
+
|
|
15
|
+
def detect(self, project_path: str) -> bool:
|
|
16
|
+
return os.path.exists(os.path.join(project_path, "Gemfile.lock"))
|
|
17
|
+
|
|
18
|
+
def list_dependencies(self, project_path: str) -> list[tuple[str, str]]:
|
|
19
|
+
path = os.path.join(project_path, "Gemfile.lock")
|
|
20
|
+
deps = []
|
|
21
|
+
in_specs = False
|
|
22
|
+
with open(path) as f:
|
|
23
|
+
for line in f:
|
|
24
|
+
stripped = line.strip()
|
|
25
|
+
if stripped == "specs:":
|
|
26
|
+
in_specs = True
|
|
27
|
+
continue
|
|
28
|
+
if in_specs:
|
|
29
|
+
if not line.startswith(" ") or line.startswith(" "):
|
|
30
|
+
# Nested entries are transitive dependencies. The
|
|
31
|
+
# scanner currently reports only top-level gems.
|
|
32
|
+
if not re.match(r"^\s{4}\S", line):
|
|
33
|
+
continue
|
|
34
|
+
match = re.match(r"^\s{4}([A-Za-z0-9_.\-]+)\s+\(([^)]+)\)", line)
|
|
35
|
+
if match:
|
|
36
|
+
deps.append((match.group(1), match.group(2)))
|
|
37
|
+
elif line.strip() == "" or not line.startswith(" "):
|
|
38
|
+
in_specs = False
|
|
39
|
+
return deps
|
|
40
|
+
|
|
41
|
+
def get_latest_version(self, name: str) -> str | None:
|
|
42
|
+
try:
|
|
43
|
+
url = f"https://rubygems.org/api/v1/versions/{name}.json"
|
|
44
|
+
with urllib.request.urlopen(url, timeout=10) as r:
|
|
45
|
+
data = json.load(r)
|
|
46
|
+
for entry in data:
|
|
47
|
+
if not entry.get("prerelease"):
|
|
48
|
+
return entry["number"]
|
|
49
|
+
return data[0]["number"] if data else None
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def get_release_date(self, name: str, version: str) -> datetime | None:
|
|
54
|
+
try:
|
|
55
|
+
url = f"https://rubygems.org/api/v1/versions/{name}.json"
|
|
56
|
+
with urllib.request.urlopen(url, timeout=10) as r:
|
|
57
|
+
data = json.load(r)
|
|
58
|
+
for entry in data:
|
|
59
|
+
if entry["number"] == version:
|
|
60
|
+
ts = entry["created_at"]
|
|
61
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
62
|
+
return None
|
|
63
|
+
except Exception:
|
|
64
|
+
return None
|