deptime 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deptime/__init__.py +3 -0
- deptime/cli.py +252 -0
- deptime/config.py +160 -0
- deptime/coverage.toml +300 -0
- deptime/index/__init__.py +1 -0
- deptime/index/distribution.py +278 -0
- deptime/index/schema.sql +62 -0
- deptime/index/store.py +323 -0
- deptime/ingest/__init__.py +1 -0
- deptime/ingest/diff.py +129 -0
- deptime/ingest/pipeline.py +119 -0
- deptime/ingest/pypi.py +205 -0
- deptime/ingest/replacements.py +74 -0
- deptime/ingest/snapshot.py +214 -0
- deptime/models.py +179 -0
- deptime/query.py +798 -0
- deptime/server/__init__.py +1 -0
- deptime/server/http_app.py +107 -0
- deptime/server/mcp_app.py +154 -0
- deptime/server/metering.py +154 -0
- deptime/tracebacks.py +205 -0
- deptime-0.0.1.dist-info/METADATA +120 -0
- deptime-0.0.1.dist-info/RECORD +26 -0
- deptime-0.0.1.dist-info/WHEEL +4 -0
- deptime-0.0.1.dist-info/entry_points.txt +2 -0
- deptime-0.0.1.dist-info/licenses/LICENSE +21 -0
deptime/__init__.py
ADDED
deptime/cli.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""`deptime` command line.
|
|
2
|
+
|
|
3
|
+
Everyday use (local, private):
|
|
4
|
+
deptime mcp run the MCP server over stdio for Claude Code / Cursor / Codex / VS Code
|
|
5
|
+
deptime explain|history|check|diff|coverage
|
|
6
|
+
deptime index pull|status manage the prebuilt index (downloaded from GitHub Releases)
|
|
7
|
+
Building the index yourself / CI:
|
|
8
|
+
deptime ingest <pkg>|--all analyse PyPI releases into a locally built index
|
|
9
|
+
deptime index pack --out DIR produce index.sqlite.gz + manifest.json for publishing
|
|
10
|
+
Hosting (optional):
|
|
11
|
+
deptime serve MCP over HTTP at /mcp plus a JSON API at /v1/*
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from deptime.config import Settings, load_coverage
|
|
22
|
+
from deptime.index.store import IndexStore
|
|
23
|
+
from deptime.models import SymbolQuery
|
|
24
|
+
from deptime.query import DepTime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _err(message: str) -> None:
|
|
28
|
+
print(message, file=sys.stderr, flush=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _dt(settings: Settings) -> DepTime:
|
|
32
|
+
"""Query-command setup: download the index on first use (synchronously), then open it read-only."""
|
|
33
|
+
if not settings.index_path.exists():
|
|
34
|
+
from deptime.index.distribution import ensure_index
|
|
35
|
+
|
|
36
|
+
if not settings.offline:
|
|
37
|
+
_err(f"deptime: no index at {settings.index_path}; downloading it once…")
|
|
38
|
+
_err(f"deptime: {ensure_index(settings).message}")
|
|
39
|
+
store = IndexStore.open(settings.index_path) if settings.index_path.exists() else None
|
|
40
|
+
return DepTime(store, load_coverage(settings.coverage_file), settings.index_path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _print(model) -> None:
|
|
44
|
+
print(model.model_dump_json(indent=2, exclude_none=True))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_ingest(args: argparse.Namespace, settings: Settings) -> int:
|
|
48
|
+
from deptime.index.distribution import mark_locally_built
|
|
49
|
+
from deptime.ingest.pipeline import ingest_package
|
|
50
|
+
|
|
51
|
+
coverage = load_coverage(settings.coverage_file)
|
|
52
|
+
names = [p.name for p in coverage.packages] if args.all else args.packages
|
|
53
|
+
if not names:
|
|
54
|
+
_err("name at least one package, or pass --all")
|
|
55
|
+
return 2
|
|
56
|
+
store = IndexStore.open(settings.index_path, readonly=False)
|
|
57
|
+
mark_locally_built(settings.index_path) # from now on this index is yours; it is never auto-replaced
|
|
58
|
+
failures = 0
|
|
59
|
+
for name in names:
|
|
60
|
+
spec = coverage.get(name)
|
|
61
|
+
if spec is None:
|
|
62
|
+
_err(f"{name}: not in the coverage list (src/deptime/coverage.toml or $DEPTIME_COVERAGE_FILE)")
|
|
63
|
+
failures += 1
|
|
64
|
+
continue
|
|
65
|
+
try:
|
|
66
|
+
report = ingest_package(
|
|
67
|
+
spec, coverage.policy, store, settings.cache_dir, versions=args.versions or None, log=_err
|
|
68
|
+
)
|
|
69
|
+
except Exception as exc: # one broken package must not stop the rest of the run
|
|
70
|
+
_err(f"[{name}] FAILED: {type(exc).__name__}: {exc}")
|
|
71
|
+
failures += 1
|
|
72
|
+
continue
|
|
73
|
+
failures += bool(report.failed)
|
|
74
|
+
if report.failed:
|
|
75
|
+
_err(json.dumps({"package": name, "failed": report.failed}, indent=2))
|
|
76
|
+
store.close()
|
|
77
|
+
return 1 if failures else 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_explain(args: argparse.Namespace, settings: Settings) -> int:
|
|
81
|
+
text = Path(args.file).read_text() if args.file else sys.stdin.read()
|
|
82
|
+
installed = dict(item.split("==", 1) for item in args.installed)
|
|
83
|
+
freeze = Path(args.freeze).read_text() if args.freeze else None
|
|
84
|
+
_print(_dt(settings).explain_error(text, installed or None, freeze))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_history(args: argparse.Namespace, settings: Settings) -> int:
|
|
89
|
+
_print(_dt(settings).symbol_history(args.package, args.path))
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cmd_check(args: argparse.Namespace, settings: Settings) -> int:
|
|
94
|
+
queries = []
|
|
95
|
+
for raw in args.symbols:
|
|
96
|
+
path, _, kw = raw.partition(":")
|
|
97
|
+
queries.append(SymbolQuery(path=path, kwargs=[k for k in kw.split(",") if k]))
|
|
98
|
+
_print(_dt(settings).check_symbols(args.package, args.version, queries))
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_diff(args: argparse.Namespace, settings: Settings) -> int:
|
|
103
|
+
_print(
|
|
104
|
+
_dt(settings).diff_versions(
|
|
105
|
+
args.package,
|
|
106
|
+
args.from_version,
|
|
107
|
+
args.to_version,
|
|
108
|
+
args.used or None,
|
|
109
|
+
args.include_added,
|
|
110
|
+
args.limit,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_coverage(args: argparse.Namespace, settings: Settings) -> int:
|
|
117
|
+
_print(_dt(settings).coverage_report())
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cmd_index(args: argparse.Namespace, settings: Settings) -> int:
|
|
122
|
+
from deptime.index import distribution as dist
|
|
123
|
+
|
|
124
|
+
if args.action == "pull":
|
|
125
|
+
local = dist.read_local_meta(settings.index_path)
|
|
126
|
+
if settings.index_path.exists() and local is None and not args.force:
|
|
127
|
+
_err(
|
|
128
|
+
f"{settings.index_path} was built locally; pass --force to replace it with the published index"
|
|
129
|
+
)
|
|
130
|
+
return 1
|
|
131
|
+
with dist.http_client() as client:
|
|
132
|
+
manifest = dist.fetch_manifest(settings.index_url, client)
|
|
133
|
+
if local and local.get("manifest", {}).get("sha256") == manifest.sha256 and not args.force:
|
|
134
|
+
_err(f"deptime: index already up to date (built {manifest.built_at})")
|
|
135
|
+
return 0
|
|
136
|
+
dist.download(settings.index_url, settings.index_path, client, manifest=manifest)
|
|
137
|
+
_err(f"deptime: installed index with {manifest.packages} packages, built {manifest.built_at}")
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
if args.action == "pack":
|
|
141
|
+
if not settings.index_path.exists():
|
|
142
|
+
_err(f"no index at {settings.index_path}; run `deptime ingest` first")
|
|
143
|
+
return 1
|
|
144
|
+
manifest = dist.pack(settings.index_path, Path(args.out))
|
|
145
|
+
print(json.dumps(manifest.__dict__, indent=2))
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
# status
|
|
149
|
+
local = dist.read_local_meta(settings.index_path)
|
|
150
|
+
status: dict[str, object] = {
|
|
151
|
+
"index_path": str(settings.index_path),
|
|
152
|
+
"exists": settings.index_path.exists(),
|
|
153
|
+
"source": "downloaded" if local else ("locally built" if settings.index_path.exists() else None),
|
|
154
|
+
"index_url": settings.index_url,
|
|
155
|
+
"offline": settings.offline,
|
|
156
|
+
}
|
|
157
|
+
if settings.index_path.exists():
|
|
158
|
+
store = IndexStore.open(settings.index_path)
|
|
159
|
+
status.update(
|
|
160
|
+
built_at=store.built_at(),
|
|
161
|
+
packages=len(store.package_rows()),
|
|
162
|
+
size_bytes=settings.index_path.stat().st_size,
|
|
163
|
+
)
|
|
164
|
+
store.close()
|
|
165
|
+
if local:
|
|
166
|
+
status.update(downloaded_at=local.get("downloaded_at"), checked_at=local.get("checked_at"))
|
|
167
|
+
print(json.dumps(status, indent=2))
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cmd_mcp(args: argparse.Namespace, settings: Settings) -> int:
|
|
172
|
+
"""Local stdio MCP server. stdout is the protocol channel: everything else goes to stderr."""
|
|
173
|
+
from deptime.index.distribution import start_background_refresh
|
|
174
|
+
from deptime.server.mcp_app import build_mcp
|
|
175
|
+
|
|
176
|
+
store = IndexStore.open(settings.index_path) if settings.index_path.exists() else None
|
|
177
|
+
dt = DepTime(store, load_coverage(settings.coverage_file), settings.index_path)
|
|
178
|
+
start_background_refresh(dt, settings)
|
|
179
|
+
build_mcp(dt).run(transport="stdio")
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cmd_serve(args: argparse.Namespace, settings: Settings) -> int:
|
|
184
|
+
import uvicorn
|
|
185
|
+
|
|
186
|
+
from deptime.server.http_app import create_app
|
|
187
|
+
|
|
188
|
+
uvicorn.run(create_app(settings), host=args.host or settings.host, port=args.port or settings.port)
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv: list[str] | None = None) -> int:
|
|
193
|
+
parser = argparse.ArgumentParser(
|
|
194
|
+
prog="deptime", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
195
|
+
)
|
|
196
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
197
|
+
|
|
198
|
+
p = sub.add_parser("mcp", help="run the MCP server over stdio (local mode)")
|
|
199
|
+
p.set_defaults(func=cmd_mcp)
|
|
200
|
+
|
|
201
|
+
p = sub.add_parser("explain", help="explain a traceback (stdin or --file)")
|
|
202
|
+
p.add_argument("--file")
|
|
203
|
+
p.add_argument("--installed", nargs="*", default=[], metavar="PKG==VER")
|
|
204
|
+
p.add_argument("--freeze", help="path to `pip freeze` output")
|
|
205
|
+
p.set_defaults(func=cmd_explain)
|
|
206
|
+
|
|
207
|
+
p = sub.add_parser("history", help="version history of one symbol")
|
|
208
|
+
p.add_argument("package")
|
|
209
|
+
p.add_argument("path")
|
|
210
|
+
p.set_defaults(func=cmd_history)
|
|
211
|
+
|
|
212
|
+
p = sub.add_parser("check", help="check symbols (path or path:kw1,kw2) at a version")
|
|
213
|
+
p.add_argument("package")
|
|
214
|
+
p.add_argument("version")
|
|
215
|
+
p.add_argument("symbols", nargs="+")
|
|
216
|
+
p.set_defaults(func=cmd_check)
|
|
217
|
+
|
|
218
|
+
p = sub.add_parser("diff", help="breaking changes between versions (from may be YYYY-MM-DD)")
|
|
219
|
+
p.add_argument("package")
|
|
220
|
+
p.add_argument("from_version")
|
|
221
|
+
p.add_argument("to_version", nargs="?")
|
|
222
|
+
p.add_argument("--used", nargs="*")
|
|
223
|
+
p.add_argument("--include-added", action="store_true")
|
|
224
|
+
p.add_argument("--limit", type=int, default=200)
|
|
225
|
+
p.set_defaults(func=cmd_diff)
|
|
226
|
+
|
|
227
|
+
p = sub.add_parser("coverage", help="what is indexed")
|
|
228
|
+
p.set_defaults(func=cmd_coverage)
|
|
229
|
+
|
|
230
|
+
p = sub.add_parser("index", help="manage the index: pull | status | pack")
|
|
231
|
+
p.add_argument("action", choices=["pull", "status", "pack"])
|
|
232
|
+
p.add_argument("--force", action="store_true", help="pull: replace a locally built or current index")
|
|
233
|
+
p.add_argument("--out", default="dist", help="pack: output directory")
|
|
234
|
+
p.set_defaults(func=cmd_index)
|
|
235
|
+
|
|
236
|
+
p = sub.add_parser("ingest", help="build or extend a local index from PyPI releases")
|
|
237
|
+
p.add_argument("packages", nargs="*")
|
|
238
|
+
p.add_argument("--all", action="store_true", help="every package in the coverage list")
|
|
239
|
+
p.add_argument("--versions", nargs="*", help="explicit versions instead of the coverage policy")
|
|
240
|
+
p.set_defaults(func=cmd_ingest)
|
|
241
|
+
|
|
242
|
+
p = sub.add_parser("serve", help="host MCP over HTTP (/mcp) plus the JSON API (/v1/*)")
|
|
243
|
+
p.add_argument("--host")
|
|
244
|
+
p.add_argument("--port", type=int)
|
|
245
|
+
p.set_defaults(func=cmd_serve)
|
|
246
|
+
|
|
247
|
+
args = parser.parse_args(argv)
|
|
248
|
+
return args.func(args, Settings.from_env())
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
raise SystemExit(main())
|
deptime/config.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Runtime settings (from environment) and the coverage list (packaged `deptime/coverage.toml`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tomllib
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import date
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from packaging.utils import canonicalize_name
|
|
13
|
+
|
|
14
|
+
# Rolling GitHub Release that CI refreshes nightly (.github/workflows/build-index.yml).
|
|
15
|
+
DEFAULT_INDEX_URL = "https://github.com/cschmertz/deptime/releases/download/index"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _env(name: str, default: str) -> str:
|
|
19
|
+
value = os.environ.get(name, "").strip()
|
|
20
|
+
return value or default
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _env_int(name: str, default: int) -> int:
|
|
24
|
+
return int(_env(name, str(default)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
28
|
+
return _env(name, "true" if default else "false").lower() in {"1", "true", "yes", "on"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def default_home() -> Path:
|
|
32
|
+
"""Where DepTime keeps its index and build cache: $DEPTIME_HOME, else $XDG_CACHE_HOME/deptime, else ~/.cache/deptime."""
|
|
33
|
+
explicit = os.environ.get("DEPTIME_HOME", "").strip()
|
|
34
|
+
if explicit:
|
|
35
|
+
return Path(explicit).expanduser()
|
|
36
|
+
xdg = os.environ.get("XDG_CACHE_HOME", "").strip()
|
|
37
|
+
return (Path(xdg).expanduser() if xdg else Path.home() / ".cache") / "deptime"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Settings:
|
|
42
|
+
index_path: Path # the SQLite index the tools read
|
|
43
|
+
cache_dir: Path # ingestion build cache: artifacts, extracted wheels, snapshots
|
|
44
|
+
coverage_file: Path | None # None = the coverage list packaged with deptime
|
|
45
|
+
index_url: str # base URL serving manifest.json + index.sqlite.gz
|
|
46
|
+
index_max_age_hours: int # how stale a *downloaded* index may get before a refresh check
|
|
47
|
+
offline: bool # never touch the network (no index download/refresh)
|
|
48
|
+
# --- hosted mode only (local stdio mode never meters) ---
|
|
49
|
+
host: str
|
|
50
|
+
port: int
|
|
51
|
+
public_url: str
|
|
52
|
+
trust_proxy_headers: bool
|
|
53
|
+
anon_daily_limit: int
|
|
54
|
+
free_key_monthly_limit: int
|
|
55
|
+
pro_monthly_limit: int
|
|
56
|
+
dev_api_keys: str # "key:plan,key2:plan"; a real key store is parked with the paid tiers
|
|
57
|
+
# With trust_proxy_headers: proxies *append* to X-Forwarded-For, so the client is this many hops from the right.
|
|
58
|
+
trusted_proxy_hops: int = 1
|
|
59
|
+
client_ip_header: str = "" # e.g. "Fly-Client-IP": a header your platform's proxy sets (wins over XFF)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_env(cls) -> Settings:
|
|
63
|
+
home = default_home()
|
|
64
|
+
coverage = os.environ.get("DEPTIME_COVERAGE_FILE", "").strip()
|
|
65
|
+
return cls(
|
|
66
|
+
index_path=Path(_env("DEPTIME_INDEX_PATH", str(home / "index.sqlite"))).expanduser(),
|
|
67
|
+
cache_dir=Path(_env("DEPTIME_CACHE_DIR", str(home / "build"))).expanduser(),
|
|
68
|
+
coverage_file=Path(coverage).expanduser() if coverage else None,
|
|
69
|
+
index_url=_env("DEPTIME_INDEX_URL", DEFAULT_INDEX_URL).rstrip("/"),
|
|
70
|
+
index_max_age_hours=_env_int("DEPTIME_INDEX_MAX_AGE_HOURS", 24),
|
|
71
|
+
offline=_env_bool("DEPTIME_OFFLINE", False),
|
|
72
|
+
host=_env("DEPTIME_HOST", "127.0.0.1"),
|
|
73
|
+
port=_env_int("DEPTIME_PORT", 8000),
|
|
74
|
+
public_url=_env("DEPTIME_PUBLIC_URL", "http://127.0.0.1:8000"),
|
|
75
|
+
trust_proxy_headers=_env_bool("DEPTIME_TRUST_PROXY_HEADERS", False),
|
|
76
|
+
anon_daily_limit=_env_int("DEPTIME_ANON_DAILY_LIMIT", 25),
|
|
77
|
+
free_key_monthly_limit=_env_int("DEPTIME_FREE_KEY_MONTHLY_LIMIT", 1500),
|
|
78
|
+
pro_monthly_limit=_env_int("DEPTIME_PRO_MONTHLY_LIMIT", 20000),
|
|
79
|
+
dev_api_keys=_env("DEPTIME_DEV_API_KEYS", ""),
|
|
80
|
+
trusted_proxy_hops=max(1, _env_int("DEPTIME_TRUSTED_PROXY_HOPS", 1)),
|
|
81
|
+
client_ip_header=_env("DEPTIME_CLIENT_IP_HEADER", ""),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class VersionPolicy:
|
|
87
|
+
since: date
|
|
88
|
+
recent_months: int
|
|
89
|
+
include_prereleases: bool
|
|
90
|
+
include_yanked: bool
|
|
91
|
+
|
|
92
|
+
def describe(self) -> str:
|
|
93
|
+
return (
|
|
94
|
+
f"first+last release of every minor series active since {self.since.isoformat()}, "
|
|
95
|
+
f"plus every release from the last {self.recent_months} months; "
|
|
96
|
+
f"pre-releases {'included' if self.include_prereleases else 'excluded'}, "
|
|
97
|
+
f"yanked {'included' if self.include_yanked else 'excluded'}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class PackageSpec:
|
|
103
|
+
name: str # canonical PEP 503 name
|
|
104
|
+
import_names: tuple[str, ...]
|
|
105
|
+
tier: str
|
|
106
|
+
notes: str = ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class Coverage:
|
|
111
|
+
policy: VersionPolicy
|
|
112
|
+
packages: tuple[PackageSpec, ...]
|
|
113
|
+
|
|
114
|
+
def get(self, name: str) -> PackageSpec | None:
|
|
115
|
+
wanted = canonicalize_name(name)
|
|
116
|
+
return next((p for p in self.packages if p.name == wanted), None)
|
|
117
|
+
|
|
118
|
+
def packages_for_module(self, module: str) -> list[PackageSpec]:
|
|
119
|
+
"""Packages whose import names are the longest dotted prefix of `module`.
|
|
120
|
+
|
|
121
|
+
`google.genai.types` -> google-genai; `agents.run` -> openai-agents. Several packages can tie
|
|
122
|
+
(import-name collisions); callers disambiguate with installed_versions.
|
|
123
|
+
"""
|
|
124
|
+
best_len, best = 0, []
|
|
125
|
+
for spec in self.packages:
|
|
126
|
+
for import_name in spec.import_names:
|
|
127
|
+
if module == import_name or module.startswith(import_name + "."):
|
|
128
|
+
n = import_name.count(".") + 1
|
|
129
|
+
if n > best_len:
|
|
130
|
+
best_len, best = n, [spec]
|
|
131
|
+
elif n == best_len and spec not in best:
|
|
132
|
+
best.append(spec)
|
|
133
|
+
return best
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def load_coverage(path: Path | None = None) -> Coverage:
|
|
137
|
+
"""Load a coverage file, or the list packaged with deptime when `path` is None."""
|
|
138
|
+
text = (
|
|
139
|
+
path.read_text(encoding="utf-8")
|
|
140
|
+
if path is not None
|
|
141
|
+
else resources.files("deptime").joinpath("coverage.toml").read_text(encoding="utf-8")
|
|
142
|
+
)
|
|
143
|
+
raw = tomllib.loads(text)
|
|
144
|
+
pol = raw.get("policy", {})
|
|
145
|
+
policy = VersionPolicy(
|
|
146
|
+
since=date.fromisoformat(pol.get("since", "2023-01-01")),
|
|
147
|
+
recent_months=int(pol.get("recent_months", 12)),
|
|
148
|
+
include_prereleases=bool(pol.get("include_prereleases", False)),
|
|
149
|
+
include_yanked=bool(pol.get("include_yanked", False)),
|
|
150
|
+
)
|
|
151
|
+
packages = tuple(
|
|
152
|
+
PackageSpec(
|
|
153
|
+
name=canonicalize_name(p["name"]),
|
|
154
|
+
import_names=tuple(p.get("import_names") or [canonicalize_name(p["name"]).replace("-", "_")]),
|
|
155
|
+
tier=p.get("tier", "B"),
|
|
156
|
+
notes=p.get("notes", ""),
|
|
157
|
+
)
|
|
158
|
+
for p in raw.get("package", [])
|
|
159
|
+
)
|
|
160
|
+
return Coverage(policy=policy, packages=packages)
|