Python API¶
Use depkeeper programmatically in your Python scripts and tools. This page documents the core modules, data models, and utility functions available for integration.
Overview¶
Core pipeline:
| Step | Module | Input | Output |
|---|---|---|---|
| 1 | RequirementsParser | requirements file | List[Requirement] |
| 2 | VersionChecker | List[Requirement] | List[Package] |
| 3 | DependencyAnalyzer | List[Package] | ResolutionResult |
| 4 | Apply / Report | ResolutionResult | Updated file or report |
PyPIDataStore backs both VersionChecker and DependencyAnalyzer, and is itself backed by HTTPClient.
depkeeper provides a Python API for:
- Parsing
requirements.txtfiles into structuredRequirementobjects - Checking package versions against PyPI with strict major version boundaries
- Resolving dependency conflicts through iterative analysis
- Formatting and displaying results
All core modules share a single PyPIDataStore instance to ensure each package is fetched at most once per process.
Quick Start¶
import asyncio
from depkeeper.core import RequirementsParser, VersionChecker, PyPIDataStore
from depkeeper.utils import HTTPClient
async def check_requirements():
# Parse requirements file
parser = RequirementsParser()
requirements = parser.parse_file("requirements.txt")
# Check versions
async with HTTPClient() as http:
store = PyPIDataStore(http)
checker = VersionChecker(data_store=store)
packages = await checker.check_packages(requirements)
# Report
for pkg in packages:
if pkg.has_update():
print(f"{pkg.name}: {pkg.current_version} -> {pkg.recommended_version}")
asyncio.run(check_requirements())
Core Modules¶
RequirementsParser¶
Stateful parser for pip-style requirements files. Supports all PEP 440/508 syntax including version specifiers, extras, environment markers, include directives (-r), constraint files (-c), VCS URLs, editable installs, and hash verification.
The parser maintains internal state across multiple parse_file calls:
- Include stack -- tracks the chain of
-rdirectives to detect circular dependencies - Constraint map -- stores requirements loaded via
-cdirectives
Call reset() to clear state before reusing the parser on an unrelated set of files.
from depkeeper.core import RequirementsParser
parser = RequirementsParser()
# Parse from file
requirements = parser.parse_file("requirements.txt")
# Parse from string
content = """
requests>=2.28.0
flask==2.3.0
-r base.txt
"""
requirements = parser.parse_string(content, source_file_path="inline")
# Access constraint files loaded via -c
constraints = parser.get_constraints()
# Reset state before reusing
parser.reset()
Methods¶
Stateful parser for pip-style requirements files.
Maintains two pieces of internal state across multiple parse_file calls:
- Include stack — tracks the chain of
-rdirectives to detect circular dependencies. - Constraint map — stores all requirements loaded via
-cdirectives; these are applied to matching package names during parsing.
Call :meth:reset to clear state before reusing the parser on an unrelated set of files.
Example::
>>> parser = RequirementsParser()
>>> reqs = parser.parse_file("requirements.txt")
>>> len(reqs)
42
>>> parser.get_constraints()
{'django': <Requirement django==3.2>}
>>> parser.reset()
Initialise the parser with empty state.
Source code in depkeeper/core/parser.py
Functions¶
parse_file ¶
parse_file(file_path: Union[str, Path], is_constraint_file: bool = False, _parent_directory_path: Optional[Path] = None) -> List[Requirement]
Parse a requirements file from disk.
Reads the file at file_path, processes all directives (-r, -c, -e, --hash), and returns a flat list of :class:Requirement objects. If file_path is relative and _parent_directory_path is provided (internal use by -r), the path is resolved relative to the parent.
Circular include chains (A.txt includes B.txt which includes A.txt) are detected and raise :exc:ParseError.
| PARAMETER | DESCRIPTION |
|---|---|
file_path | Path to the requirements file (absolute or relative). TYPE: |
is_constraint_file | If TYPE: |
_parent_directory_path | Internal parameter used when resolving TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Requirement] | List of :class: |
List[Requirement] | is_constraint_file is |
| RAISES | DESCRIPTION |
|---|---|
FileOperationError | The file does not exist or cannot be read. |
ParseError | A circular include was detected or the file contains invalid syntax. |
Example::
>>> parser = RequirementsParser()
>>> reqs = parser.parse_file("requirements/prod.txt")
>>> [r.name for r in reqs if r.editable]
['my-local-package']
Source code in depkeeper/core/parser.py
| Python | |
|---|---|
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
parse_string ¶
parse_string(requirements_content: str, source_file_path: Optional[str] = None, is_constraint_file: bool = False, _current_directory_path: Optional[Path] = None) -> List[Requirement]
Parse requirements from raw text content.
Splits requirements_content into lines and processes each via :meth:parse_line. Requirements loaded from -r includes are flattened into the result list.
| PARAMETER | DESCRIPTION |
|---|---|
requirements_content | Multi-line requirements text. TYPE: |
source_file_path | Optional file path for error messages (purely informational; does not affect parsing). TYPE: |
is_constraint_file | If TYPE: |
_current_directory_path | Internal parameter; the directory containing the "file" being parsed (used to resolve relative TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Requirement] | List of :class: |
Example::
>>> content = """
... flask>=2.0
... # A comment
... requests>=2.25.0
... """
>>> parser = RequirementsParser()
>>> reqs = parser.parse_string(content)
>>> [r.name for r in reqs]
['flask', 'requests']
Source code in depkeeper/core/parser.py
| Python | |
|---|---|
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | |
parse_line ¶
parse_line(line_text: str, line_number: int, source_file_path: Optional[str] = None, _current_directory_path: Optional[Path] = None) -> Optional[Union[Requirement, List[Requirement]]]
Parse a single line from a requirements file.
Handles all pip-supported line types:
- Blank lines and
#comments →None -r file.txt→List[Requirement](nested parse)-c file.txt→None(side-effect: populates constraints)-e <url-or-path>→ editable :class:Requirementpkg==1.0 --hash sha256:...→ :class:Requirementwith hashes- Standard PEP 508 specs → :class:
Requirement
| PARAMETER | DESCRIPTION |
|---|---|
line_text | Raw line text (may include leading/trailing whitespace). TYPE: |
line_number | Line number (1-indexed) for error reporting. TYPE: |
source_file_path | Optional source file path for error messages. TYPE: |
_current_directory_path | Internal; directory of the file being parsed (used to resolve relative TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Optional[Union[Requirement, List[Requirement]]] |
|
Optional[Union[Requirement, List[Requirement]]] |
|
Optional[Union[Requirement, List[Requirement]]] |
|
| RAISES | DESCRIPTION |
|---|---|
ParseError | The line contains invalid syntax or a directive that cannot be processed. |
Example::
>>> parser = RequirementsParser()
>>> parser.parse_line("requests>=2.25.0", 1)
<Requirement requests>=2.25.0>
>>> parser.parse_line("# comment", 2) is None
True
>>> parser.parse_line("-r base.txt", 3) # returns List[Requirement]
Source code in depkeeper/core/parser.py
| Python | |
|---|---|
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
get_constraints ¶
Return a copy of all constraint requirements loaded via -c.
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, Requirement] | Dictionary mapping normalised package names to their constraint |
Dict[str, Requirement] | class: |
Example::
>>> parser = RequirementsParser()
>>> parser.parse_file("requirements.txt") # includes -c constraints.txt
>>> constraints = parser.get_constraints()
>>> constraints.get("django")
<Requirement django==3.2>
Source code in depkeeper/core/parser.py
reset ¶
Clear all internal state (include stack and constraints).
Call this before reusing the parser on a new, unrelated set of files to prevent cross-contamination.
Example::
>>> parser = RequirementsParser()
>>> parser.parse_file("projectA/requirements.txt")
>>> parser.reset()
>>> parser.parse_file("projectB/requirements.txt") # clean slate
Source code in depkeeper/core/parser.py
VersionChecker¶
Async package version checker with strict major version boundary enforcement. Recommendations never cross major version boundaries -- if the current version is 2.x.x, the recommended version will always be 2.y.z, never 3.0.0.
All network I/O is delegated to PyPIDataStore, which guarantees that each unique package is fetched at most once.
from depkeeper.core import VersionChecker, PyPIDataStore
from depkeeper.utils import HTTPClient
async def main():
async with HTTPClient() as http:
store = PyPIDataStore(http)
checker = VersionChecker(data_store=store)
# Check single package
pkg = await checker.get_package_info("requests", current_version="2.28.0")
print(f"Latest: {pkg.latest_version}")
print(f"Recommended: {pkg.recommended_version}")
# Check multiple packages concurrently
packages = await checker.check_packages(requirements)
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
data_store | PyPIDataStore | Required | Shared PyPI metadata cache |
infer_version_from_constraints | bool | True | Infer current version from range constraints like >=2.0 |
Methods¶
Async package version checker with strict major version boundaries.
Fetches metadata from PyPI (via the shared data store) and determines the highest Python-compatible version for each package, strictly respecting major-version boundaries when a current version is known. Unlike the base implementation, this checker will never recommend crossing a major version boundary, even if a newer major exists.
All network I/O is delegated to data_store, which guarantees that each unique package is fetched at most once.
| PARAMETER | DESCRIPTION |
|---|---|
data_store | Shared PyPI metadata cache. Required. TYPE: |
infer_version_from_constraints | When TYPE: |
| RAISES | DESCRIPTION |
|---|---|
TypeError | If data_store is |
Example::
>>> async with HTTPClient() as http:
... store = PyPIDataStore(http)
... checker = VersionChecker(data_store=store)
... pkg = await checker.get_package_info("flask", current_version="2.0.0")
... print(pkg.recommended_version)
'2.3.3' # Never 3.x.x, even if 3.0.0 is available
Source code in depkeeper/core/checker.py
Functions¶
get_package_info async ¶
Fetch metadata and compute a recommended version for name.
CRITICAL: Recommendations NEVER cross major version boundaries. If current_version is 2.x.x, the recommended version will be 2.y.z (never 3.0.0), even if 3.0.0 is the latest available version on PyPI.
Calls :meth:PyPIDataStore.get_package_data (which may trigger a network fetch or return cached data), then applies the strict major-boundary recommendation algorithm to choose the best upgrade target.
| PARAMETER | DESCRIPTION |
|---|---|
name | Package name (any casing / separator style). TYPE: |
current_version | The version currently installed (if known). When provided, the recommendation stays within the same major version. If no compatible version exists in that major, stays on current version rather than crossing the boundary. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
A | class: TYPE: |
Package |
|
| RAISES | DESCRIPTION |
|---|---|
PyPIError | The package does not exist on PyPI or the API returned an unexpected status. |
Example::
>>> pkg = await checker.get_package_info("requests", current_version="2.25.0")
>>> pkg.latest_version
'2.31.0'
>>> pkg.recommended_version
'2.31.0' # Stays in major version 2
Source code in depkeeper/core/checker.py
check_packages async ¶
Check multiple packages concurrently.
For each requirement, extracts the current version (via :meth:extract_current_version) and calls :meth:get_package_info. Errors for individual packages are caught and replaced with unavailable stubs so that one bad package does not block the rest.
| PARAMETER | DESCRIPTION |
|---|---|
requirements | Parsed requirements from a requirements file. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[Package] | List of :class: |
Example::
>>> requirements = parser.parse_file("requirements.txt")
>>> packages = await checker.check_packages(requirements)
>>> [p.name for p in packages if p.recommended_version]
['flask', 'requests', 'click']
Source code in depkeeper/core/checker.py
extract_current_version ¶
Infer a "current" version from a requirement's version specifiers.
Heuristic:
- If the requirement has exactly one specifier and it is
==, return that version (pinned). - If :attr:
infer_version_from_constraintsisFalse, stop here. - Otherwise, scan for the first
>=,>, or~=specifier and return its version. This treats>=2.0as "currently on 2.0" for major-version boundary purposes.
| PARAMETER | DESCRIPTION |
|---|---|
req | A parsed :class: TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Optional[str] | The inferred version string, or |
Optional[str] | possible. |
Example::
>>> req1 = Requirement(name="flask", specs=[("==", "2.0.0")], ...)
>>> checker.extract_current_version(req1)
'2.0.0'
>>> req2 = Requirement(name="flask", specs=[(">=", "2.0"), ("<", "3")], ...)
>>> checker.extract_current_version(req2)
'2.0'
>>> req3 = Requirement(name="flask", specs=[], ...)
>>> checker.extract_current_version(req3) is None
True
Source code in depkeeper/core/checker.py
PyPIDataStore¶
Async-safe, per-process cache for PyPI package metadata. Each unique package name triggers at most one HTTP request to /pypi/{pkg}/json. A semaphore limits concurrent outbound fetches, and double-checked locking prevents duplicate requests when multiple coroutines request the same package simultaneously.
from depkeeper.core import PyPIDataStore
from depkeeper.utils import HTTPClient
async def main():
async with HTTPClient() as http:
store = PyPIDataStore(http, concurrent_limit=10)
# Fetch package data
data = await store.get_package_data("requests")
print(f"Latest: {data.latest_version}")
print(f"All versions: {data.all_versions[:5]}")
# Prefetch multiple packages concurrently
await store.prefetch_packages(["flask", "click", "jinja2"])
# Get cached data (no network call)
cached = store.get_cached_package("flask")
# Get dependencies for a specific version
deps = await store.get_version_dependencies("flask", "2.3.0")
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
http_client | HTTPClient | Required | Pre-configured async HTTP client |
concurrent_limit | int | 10 | Maximum concurrent PyPI fetches |
Methods¶
Async-safe, per-process cache for PyPI package metadata.
Each unique (normalised) package name triggers at most one HTTP request to /pypi/{pkg}/json. A :class:asyncio.Semaphore limits concurrent outbound fetches, and a double-checked lock inside the semaphore prevents thundering-herd duplicates when several coroutines request the same package simultaneously.
| PARAMETER | DESCRIPTION |
|---|---|
http_client | A pre-configured :class: TYPE: |
concurrent_limit | Maximum number of PyPI fetches that may be in-flight at once. Defaults to TYPE: |
Example::
async with HTTPClient() as client:
store = PyPIDataStore(client, concurrent_limit=5)
# warm the cache for several packages at once
await store.prefetch_packages(["flask", "click", "jinja2"])
# subsequent calls return instantly from cache
flask = await store.get_package_data("flask")
print(flask.latest_version)
Source code in depkeeper/core/data_store.py
Functions¶
get_package_data async ¶
Fetch (or return cached) metadata for name.
Uses double-checked locking: the first check is lock-free; if the package is missing a second check runs inside the semaphore so that only one coroutine actually performs the HTTP call.
| PARAMETER | DESCRIPTION |
|---|---|
name | PyPI package name (any casing / underscore style). TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
A | class: TYPE: |
PyPIPackageData | JSON response. |
| RAISES | DESCRIPTION |
|---|---|
PyPIError | The package does not exist on PyPI or the API returned an unexpected status code. |
Example::
>>> data = await store.get_package_data("Requests")
>>> data.name
'requests'
>>> data.latest_version
'2.31.0'
Source code in depkeeper/core/data_store.py
prefetch_packages async ¶
Concurrently warm the cache for a batch of packages.
Errors for individual packages are silenced so that one bad package name does not prevent the rest from being cached.
| PARAMETER | DESCRIPTION |
|---|---|
names | Package names to prefetch. TYPE: |
Example::
>>> await store.prefetch_packages(["numpy", "pandas", "scipy"])
# subsequent get_package_data calls for these return instantly
Source code in depkeeper/core/data_store.py
get_version_dependencies async ¶
Return the base dependencies for a specific version of name.
Resolution order (fastest first):
- Per-version dependency cache (
_version_deps_cache). - Already-populated fields inside the cached :class:
PyPIPackageData(latest_dependenciesordependencies_cache). - A targeted
/pypi/{name}/{version}/jsonfetch, guarded by the semaphore and a second cache check.
| PARAMETER | DESCRIPTION |
|---|---|
name | Package name. TYPE: |
version | Exact version string, e.g. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[str] | List of PEP-508 dependency specifiers with extras and |
List[str] | environment markers stripped. |
Example::
>>> deps = await store.get_version_dependencies("flask", "2.3.0")
>>> deps
['Werkzeug>=2.0', 'Jinja2>=3.0', ...]
Source code in depkeeper/core/data_store.py
get_cached_package ¶
Return cached data for name without triggering a fetch.
| PARAMETER | DESCRIPTION |
|---|---|
name | Package name (any casing / underscore style). TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Optional[PyPIPackageData] | The cached :class: |
Optional[PyPIPackageData] | package has not been fetched yet. |
Example::
>>> store.get_cached_package("flask") # after a prior fetch
PyPIPackageData(name='flask', latest_version='3.0.0', ...)
>>> store.get_cached_package("unknown")
None
Source code in depkeeper/core/data_store.py
get_versions ¶
Return cached stable versions for name (newest first).
Returns an empty list when name has not been fetched yet.
| PARAMETER | DESCRIPTION |
|---|---|
name | Package name. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
List[str] | List of version strings, or |
Example::
>>> store.get_versions("flask")
['3.0.0', '2.3.3', '2.3.2', ...]
Source code in depkeeper/core/data_store.py
PyPIPackageData¶
Immutable-by-convention snapshot of one PyPI package, populated by PyPIDataStore. Contains version lists, Python compatibility data, and dependency caches.
Attributes¶
| Attribute | Type | Description |
|---|---|---|
name | str | Normalized package name |
latest_version | Optional[str] | Latest version on PyPI |
latest_requires_python | Optional[str] | Python requirement for latest version |
latest_dependencies | List[str] | Base dependencies of latest version |
all_versions | List[str] | Stable versions, newest first |
parsed_versions | List[Tuple[str, Version]] | Parsed version objects, descending |
python_requirements | Dict[str, Optional[str]] | Version to requires_python mapping |
dependencies_cache | Dict[str, List[str]] | Per-version dependency lists |
Methods¶
| Method | Returns | Description |
|---|---|---|
get_versions_in_major(major) | List[str] | Stable versions sharing a given major number |
is_python_compatible(version, python_version) | bool | Check if a package version supports a Python version |
get_python_compatible_versions(python_version, major=None) | List[str] | Stable versions compatible with a Python version |
DependencyAnalyzer¶
Resolves dependency conflicts with strict major version boundary enforcement. The analyzer builds a dependency graph, detects version conflicts, and iteratively adjusts recommendations until a conflict-free set is found or the iteration limit is reached.
from depkeeper.core import DependencyAnalyzer, PyPIDataStore
from depkeeper.utils import HTTPClient
async def main():
async with HTTPClient() as http:
store = PyPIDataStore(http)
analyzer = DependencyAnalyzer(data_store=store)
# Resolve conflicts
result = await analyzer.resolve_and_annotate_conflicts(packages)
# Check results
for name, resolution in result.resolved_versions.items():
print(f"{name}: {resolution.original} -> {resolution.resolved}")
print(f" Status: {resolution.status}")
# Summary
print(result.summary())
Methods¶
Detect and resolve version conflicts with strict major version boundaries.
This analyzer enhances the base conflict resolution by ensuring that no package is ever upgraded or downgraded across major version boundaries during conflict resolution. This prevents breaking changes from being inadvertently introduced.
The analyzer works exclusively through a :class:PyPIDataStore instance, which guarantees that every /pypi/{pkg}/json call is made at most once. All public entry points are async.
| PARAMETER | DESCRIPTION |
|---|---|
data_store | Shared PyPI data store. Required — the class has no independent HTTP path. TYPE: |
concurrent_limit | Upper bound on in-flight PyPI fetches. Forwarded to the internal semaphore. Defaults to TYPE: |
| RAISES | DESCRIPTION |
|---|---|
TypeError | If data_store is |
Example::
>>> async with HTTPClient() as http:
... store = PyPIDataStore(http)
... analyzer = DependencyAnalyzer(data_store=store)
... result = await analyzer.resolve_and_annotate_conflicts(pkgs)
... print(result.summary())
Source code in depkeeper/core/dependency_analyzer.py
Functions¶
resolve_and_annotate_conflicts async ¶
Resolve conflicts while strictly respecting major version boundaries.
Algorithm outline:
- Build an update set mapping each package name to its proposed version (
recommended_versionif available, otherwisecurrent_version). Recommended versions already respect major version boundaries. - Prefetch metadata for every package in one concurrent burst.
- Loop up to :data:
_MAX_RESOLUTION_ITERATIONStimes:
a. Scan for cross-conflicts in the current update set. b. If none remain, stop — the set is self-consistent. c. Attempt resolution within major version boundaries only:
- Try to find a compatible source version within its current major
- If that fails, try to constrain the target within its current major
- If both fail, revert both packages to their current versions
d. Break early when no progress is made.
- Annotate each :class:
Packagewith its final version and any unresolved :class:Conflictobjects. - Return a :class:
ResolutionResultwith complete details.
| PARAMETER | DESCRIPTION |
|---|---|
packages | Mutable list of :class: TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
ResolutionResult | class: |
ResolutionResult | package, conflict details, and resolution statistics. |
Example::
>>> result = await analyzer.resolve_and_annotate_conflicts(pkgs)
>>> print(result.summary())
>>> for pkg_name, info in result.resolved_versions.items():
... if info.was_changed():
... print(f"{pkg_name}: {info.original} → {info.resolved}")
Source code in depkeeper/core/dependency_analyzer.py
| Python | |
|---|---|
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 | |
ResolutionResult¶
Complete result of dependency conflict resolution, returned by DependencyAnalyzer.resolve_and_annotate_conflicts().
Attributes¶
| Attribute | Type | Description |
|---|---|---|
resolved_versions | Dict[str, PackageResolution] | Package name to resolution details |
total_packages | int | Total packages analyzed |
packages_with_conflicts | int | Packages that have conflicts |
iterations_used | int | Resolution iterations performed |
converged | bool | Whether resolution reached a stable state |
Methods¶
| Method | Returns | Description |
|---|---|---|
get_changed_packages() | List[PackageResolution] | Packages whose version was changed |
get_conflicts() | List[PackageResolution] | Packages with unresolved conflicts |
summary() | str | Human-readable resolution summary |
PackageResolution¶
Resolution details for a single package within a ResolutionResult.
Attributes¶
| Attribute | Type | Description |
|---|---|---|
name | str | Package name (normalized) |
original | Optional[str] | Initially proposed version |
resolved | Optional[str] | Final version after resolution |
status | ResolutionStatus | Why this version was chosen |
conflicts | List[Conflict] | Conflicts affecting this package |
compatible_alternative | Optional[str] | Best alternative version, if any |
Resolution Statuses¶
| Status | Description |
|---|---|
KEPT_RECOMMENDED | Original recommendation was conflict-free |
UPGRADED | Successfully upgraded to a newer version |
DOWNGRADED | Had to downgrade due to conflicts |
KEPT_CURRENT | No safe upgrade found; stayed at current |
CONSTRAINED | Version was constrained by another package |
Models¶
Requirement¶
Represents a single requirement line from a requirements file.
from depkeeper.models import Requirement
req = Requirement(
name="requests",
specs=[(">=", "2.28.0"), ("<", "3.0.0")],
extras=["security"],
markers="python_version >= '3.8'",
)
# Convert to string
print(req.to_string()) # requests[security]>=2.28.0,<3.0.0; python_version >= '3.8'
# Update version (replaces all specifiers with ==new_version)
updated = req.update_version("2.31.0")
print(updated) # requests[security]==2.31.0; python_version >= '3.8'
Attributes¶
| Attribute | Type | Description |
|---|---|---|
name | str | Canonical package name |
specs | List[Tuple[str, str]] | Version specifiers (operator, version) |
extras | List[str] | Optional extras to install |
markers | Optional[str] | Environment marker expression (PEP 508) |
url | Optional[str] | Direct URL or VCS source |
editable | bool | Whether this is an editable install (-e) |
hashes | List[str] | Hash values for verification |
comment | Optional[str] | Inline comment without the # prefix |
line_number | int | Original line number in the source file |
raw_line | Optional[str] | Original unmodified line text |
Methods¶
| Method | Returns | Description |
|---|---|---|
to_string(include_hashes=True, include_comment=True) | str | Render canonical requirements.txt representation |
update_version(new_version) | str | Return requirement string updated to a new version |
Package¶
Represents a Python package with version state, update recommendations, and conflict tracking.
from depkeeper.models import Package
pkg = Package(
name="requests",
current_version="2.28.0",
latest_version="2.32.0",
recommended_version="2.32.0",
)
# Check status
print(pkg.has_update()) # True
print(pkg.requires_downgrade) # False
print(pkg.has_conflicts()) # False
# Serialization
print(pkg.to_json())
Attributes¶
| Attribute | Type | Description |
|---|---|---|
name | str | Normalized package name (PEP 503) |
current_version | Optional[str] | Currently installed or specified version |
latest_version | Optional[str] | Latest version on PyPI (informational) |
recommended_version | Optional[str] | Safe upgrade version within major boundary |
metadata | Dict[str, Any] | Package metadata from PyPI |
conflicts | List[Conflict] | Dependency conflicts affecting this package |
Properties and Methods¶
| Member | Type | Description |
|---|---|---|
current | Optional[Version] | Parsed current version |
latest | Optional[Version] | Parsed latest version |
recommended | Optional[Version] | Parsed recommended version |
requires_downgrade | bool | True if recommended version is lower than current |
has_update() | bool | True if recommended version is newer than current |
has_conflicts() | bool | True if dependency conflicts exist |
set_conflicts(conflicts, resolved_version=None) | None | Set conflicts and optionally update recommended version |
get_conflict_summary() | List[str] | Short, user-friendly conflict summaries |
get_conflict_details() | List[str] | Detailed conflict descriptions |
get_status_summary() | Tuple[str, str, str, Optional[str]] | Status, installed, latest, recommended |
to_json() | Dict[str, Any] | JSON-safe package representation |
Conflict¶
Represents a dependency conflict between two packages. This is a frozen dataclass (immutable after creation).
from depkeeper.models import Conflict
conflict = Conflict(
source_package="flask",
target_package="werkzeug",
required_spec=">=2.0,<3.0",
conflicting_version="3.0.0",
source_version="2.3.0",
)
print(conflict.to_display_string())
# flask==2.3.0 requires werkzeug>=2.0,<3.0
Attributes¶
| Attribute | Type | Description |
|---|---|---|
source_package | str | Package declaring the dependency |
target_package | str | Package being constrained |
required_spec | str | Version specifier required by the source |
conflicting_version | str | Version that violates the requirement |
source_version | Optional[str] | Version of the source package |
Methods¶
| Method | Returns | Description |
|---|---|---|
to_display_string() | str | Human-readable conflict description |
to_short_string() | str | Compact conflict summary |
to_json() | Dict[str, Optional[str]] | JSON-serializable representation |
ConflictSet¶
Collection of conflicts affecting a single package. Provides utilities to find compatible versions.
Attributes¶
| Attribute | Type | Description |
|---|---|---|
package_name | str | Name of the affected package |
conflicts | List[Conflict] | Conflicts associated with this package |
Methods¶
| Method | Returns | Description |
|---|---|---|
add_conflict(conflict) | None | Add a conflict to the set |
has_conflicts() | bool | True if any conflicts exist |
get_max_compatible_version(available_versions) | Optional[str] | Highest version compatible with all conflicts |
Utilities¶
HTTPClient¶
Async HTTP client with retry logic, rate limiting, concurrency control, and PyPI-specific error handling. Uses httpx with HTTP/2 support.
from depkeeper.utils import HTTPClient
async def main():
async with HTTPClient(timeout=30, max_retries=3) as http:
# GET request
response = await http.get("https://pypi.org/pypi/requests/json")
# GET with JSON parsing
data = await http.get_json("https://pypi.org/pypi/requests/json")
# Batch concurrent JSON fetches
results = await http.batch_get_json([
"https://pypi.org/pypi/flask/json",
"https://pypi.org/pypi/click/json",
])
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
timeout | int | 30 | Request timeout in seconds |
max_retries | int | 3 | Maximum retry attempts |
rate_limit_delay | float | 0.0 | Minimum delay between requests |
verify_ssl | bool | True | Verify SSL certificates |
user_agent | Optional[str] | Auto-generated | Custom User-Agent header |
max_concurrency | int | 10 | Maximum concurrent requests |
Methods¶
| Method | Returns | Description |
|---|---|---|
get(url) | httpx.Response | GET request with retry logic |
post(url) | httpx.Response | POST request with retry logic |
get_json(url) | Dict[str, Any] | GET and parse JSON response |
batch_get_json(urls) | Dict[str, Dict[str, Any]] | Concurrent JSON fetches |
close() | None | Close the HTTP client |
Console Utilities¶
User-facing output helpers built on the Rich library.
from depkeeper.utils import (
print_success,
print_error,
print_warning,
print_table,
confirm,
get_raw_console,
)
# Status messages
print_success("Operation completed!")
print_error("Something went wrong")
print_warning("Proceed with caution")
# Rich table from list of dicts
print_table(
data=[
{"Name": "requests", "Version": "2.31.0"},
{"Name": "flask", "Version": "2.3.3"},
],
title="Packages",
)
# User confirmation prompt
if confirm("Apply 5 updates?", default=False):
print("Applying...")
# Access underlying Rich Console
console = get_raw_console()
Functions¶
| Function | Parameters | Description |
|---|---|---|
print_success(message, prefix="[OK]") | str | Print a styled success message |
print_error(message, prefix="[ERROR]") | str | Print a styled error message |
print_warning(message, prefix="[WARNING]") | str | Print a styled warning message |
print_table(data, headers=None, title=None, ...) | List[Dict] | Render data as a Rich table |
confirm(message, default=False) | str, bool | Prompt for yes/no confirmation |
get_raw_console() | Return the underlying Rich Console instance | |
reconfigure_console() | None | Reset the global console (useful after changing NO_COLOR) |
colorize_update_type(update_type) | str | Return Rich-markup colored update type label |
Filesystem Utilities¶
Safe file I/O helpers with backup, restore, and path validation support.
from depkeeper.utils import (
safe_read_file,
safe_write_file,
create_backup,
restore_backup,
create_timestamped_backup,
find_requirements_files,
validate_path,
)
# Read a file safely (with size limit)
content = safe_read_file("requirements.txt")
# Write with automatic backup
backup_path = safe_write_file("requirements.txt", new_content, create_backup=True)
# Find all requirements files in a directory
files = find_requirements_files(".", recursive=True)
# Validate a path stays within a base directory
resolved = validate_path("../requirements.txt", base_dir="/project")
Functions¶
| Function | Returns | Description |
|---|---|---|
safe_read_file(file_path, max_size=None, encoding="utf-8") | str | Read a text file with optional size limit |
safe_write_file(file_path, content, create_backup=True) | Optional[Path] | Atomic write with optional backup; returns backup path |
create_backup(file_path) | Path | Create a timestamped backup of a file |
restore_backup(backup_path, target_path=None) | None | Restore a file from a backup |
create_timestamped_backup(file_path) | Path | Create a backup with {stem}.{timestamp}.backup{suffix} format |
find_requirements_files(directory=".", recursive=True) | List[Path] | Find requirements files in a directory |
validate_path(path, base_dir=None) | Path | Resolve and validate a path; raises FileOperationError if outside base_dir |
Logging Utilities¶
Centralized logging setup for depkeeper.
from depkeeper.utils import (
get_logger,
setup_logging,
disable_logging,
is_logging_configured,
)
# Get a named logger
logger = get_logger("my_module")
logger.info("Processing started")
# Configure logging level
setup_logging(verbosity=2) # DEBUG level
# Check if logging has been configured
if not is_logging_configured():
setup_logging(verbosity=0)
# Suppress all logging output
disable_logging()
Functions¶
| Function | Returns | Description |
|---|---|---|
get_logger(name=None) | logging.Logger | Get a named logger under the depkeeper namespace |
setup_logging(verbosity=0) | None | Configure logging level (0=WARNING, 1=INFO, 2=DEBUG) |
is_logging_configured() | bool | Check if logging has already been set up |
disable_logging() | None | Suppress all depkeeper log output |
Version Utilities¶
Helpers for classifying version changes using PEP 440 parsing.
from depkeeper.utils import get_update_type
get_update_type("1.0.0", "2.0.0") # "major"
get_update_type("1.0.0", "1.1.0") # "minor"
get_update_type("1.0.0", "1.0.1") # "patch"
get_update_type(None, "1.0.0") # "new"
get_update_type("1.0.0", "1.0.0") # "same"
get_update_type("2.0.0", "1.0.0") # "downgrade"
Functions¶
| Function | Returns | Description |
|---|---|---|
get_update_type(current_version, target_version) | str | Classify the update type between two versions |
Return values: "major", "minor", "patch", "new", "same", "downgrade", "update", "unknown"
Exceptions¶
All exceptions inherit from DepKeeperError and support structured metadata via the details attribute.
Exception hierarchy:
DepKeeperError(base)ParseErrorNetworkErrorPyPIError
FileOperationError
| Exception | Description | Key Attributes |
|---|---|---|
DepKeeperError | Base exception for all depkeeper errors | message, details |
ParseError | Requirements file parsing failures | line_number, line_content, file_path |
NetworkError | HTTP or network operation failures | url, status_code, response_body |
PyPIError | PyPI API-specific failures | package_name (inherits NetworkError) |
FileOperationError | File system operation failures | file_path, operation, original_error |
Complete Example¶
#!/usr/bin/env python3
"""Check and update dependencies programmatically."""
import asyncio
from depkeeper.core import (
RequirementsParser,
VersionChecker,
DependencyAnalyzer,
PyPIDataStore,
)
from depkeeper.utils import HTTPClient
async def analyze_requirements(file_path: str):
"""Analyze a requirements file and report on updates."""
# Step 1: Parse requirements
parser = RequirementsParser()
requirements = parser.parse_file(file_path)
print(f"Found {len(requirements)} packages\n")
async with HTTPClient() as http:
# Step 2: Create shared data store
store = PyPIDataStore(http)
# Step 3: Check versions
checker = VersionChecker(data_store=store)
packages = await checker.check_packages(requirements)
# Step 4: Resolve conflicts
analyzer = DependencyAnalyzer(data_store=store)
result = await analyzer.resolve_and_annotate_conflicts(packages)
# Step 5: Report results
print(f"Converged: {result.converged} ({result.iterations_used} iterations)")
print(f"Conflicts: {result.packages_with_conflicts}\n")
for pkg in packages:
if pkg.has_update():
print(f" {pkg.name}: {pkg.current_version} -> {pkg.recommended_version}")
return packages
if __name__ == "__main__":
asyncio.run(analyze_requirements("requirements.txt"))
Configuration¶
DepKeeperConfig¶
Dataclass representing a parsed and validated configuration file. All fields carry defaults, so an empty or missing configuration file produces a fully usable config object.
from depkeeper.config import load_config, discover_config_file
# Auto-discover and load (depkeeper.toml or pyproject.toml)
config = load_config()
# Load from explicit path
config = load_config(Path("/project/depkeeper.toml"))
# Access values
print(config.check_conflicts) # True
print(config.strict_version_matching) # False
Functions¶
Configuration file loader for depkeeper.
Handles discovery, loading, parsing, and validation of configuration files. Supports two formats:
depkeeper.toml— settings under[depkeeper]tablepyproject.toml— settings under[tool.depkeeper]table
Discovery order:
- Explicit path from
--configorDEPKEEPER_CONFIG depkeeper.tomlin current directorypyproject.tomlwith[tool.depkeeper]section
Configuration precedence: defaults < config file < environment < CLI args.
Typical usage::
config = load_config() # Auto-discover
config = load_config(Path("custom.toml")) # Explicit path
Example (depkeeper.toml)::
[depkeeper]
check_conflicts = true
strict_version_matching = false
Functions¶
discover_config_file ¶
Find the configuration file to load.
Search order:
explicit_path(from--configorDEPKEEPER_CONFIG)depkeeper.tomlin current directorypyproject.tomlwith[tool.depkeeper]section in current directory
Validates pyproject.toml contains depkeeper section before using it.
| PARAMETER | DESCRIPTION |
|---|---|
explicit_path | Explicit config path. If provided, must exist. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Optional[Path] | Resolved path to config file, or |
| RAISES | DESCRIPTION |
|---|---|
ConfigError | Explicit path provided but does not exist. |
Source code in depkeeper/config.py
load_config ¶
load_config(config_path: Optional[Path] = None) -> DepKeeperConfig
Load and validate depkeeper configuration.
Discovers config file (or uses provided path), parses and validates it. Returns config with defaults if no file found.
Handles both depkeeper.toml and pyproject.toml formats.
| PARAMETER | DESCRIPTION |
|---|---|
config_path | Explicit path to config file. If TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Validated | class: TYPE: |
| RAISES | DESCRIPTION |
|---|---|
ConfigError | File cannot be parsed, has unknown keys, or invalid values. |
Source code in depkeeper/config.py
Class¶
DepKeeperConfig dataclass ¶
DepKeeperConfig(check_conflicts: bool = DEFAULT_CHECK_CONFLICTS, strict_version_matching: bool = DEFAULT_STRICT_VERSION_MATCHING, source_path: Optional[Path] = None)
Parsed and validated depkeeper configuration.
Contains settings from depkeeper.toml or pyproject.toml. All fields have defaults, so empty config files are valid.
| ATTRIBUTE | DESCRIPTION |
|---|---|
check_conflicts | Enable dependency conflict resolution. When TYPE: |
strict_version_matching | Only consider exact pins ( TYPE: |
source_path | Path to loaded config file, or TYPE: |
Functions¶
to_log_dict ¶
Return configuration as dictionary for debug logging.
Excludes source_path metadata.
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, Any] | Dictionary of configuration option names to values. |
Source code in depkeeper/config.py
Exception¶
ConfigError ¶
Bases: DepKeeperError
Raised when a configuration file is invalid or cannot be loaded.
| PARAMETER | DESCRIPTION |
|---|---|
message | Human-readable description of the configuration problem. TYPE: |
config_path | Path to the configuration file that caused the error. TYPE: |
option | The specific configuration option that is invalid, if applicable. TYPE: |
Source code in depkeeper/exceptions.py
See Also¶
- Getting Started -- Quick start guide
- CLI Reference -- Command-line interface
- Configuration Guide -- Configuration guide
- Configuration Options -- Full options reference
- Contributing -- Development guide