pydantic-pkgr


Namepydantic-pkgr JSON
Version 0.5.4 PyPI version JSON
download
home_pageNone
SummarySystem package manager APIs in strongly typed Python
upload_time2024-10-21 10:43:59
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords pydantic packagemanager apt brew pip system dependencies
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <h1><a href="https://github.com/ArchiveBox/pydantic-pkgr"><code>pydantic-pkgr</code></a> &nbsp; &nbsp; &nbsp; &nbsp; 📦  <small><code>apt</code>&nbsp; <code>brew</code>&nbsp; <code>pip</code>&nbsp; <code>npm</code> &nbsp;₊₊₊</small><br/><sub>Simple Pydantic interfaces for package managers + installed binaries.</sub></h1>
<br/>

[![PyPI][pypi-badge]][pypi]
[![Python Version][version-badge]][pypi]
[![Django Version][django-badge]][pypi]
[![GitHub][licence-badge]][licence]
[![GitHub Last Commit][repo-badge]][repo]
<!--[![Downloads][downloads-badge]][pypi]-->

<br/>

**It's an ORM for your package managers, providing a nice python types for packages + installers.**  
  
**This is a [Python library](https://pypi.org/project/pydantic-pkgr/) for installing & managing packages locally with a variety of package managers.**  
It's designed for when `pip` dependencies aren't enough, and your app has to check for & install dependencies at runtime.  


```shell
pip install pydantic-pkgr
```


> ✨ Built with [`pydantic`](https://pydantic-docs.helpmanual.io/) v2 for strong static typing guarantees and json import/export compatibility  
> 📦 Provides consistent cross-platform interfaces for dependency resolution & installation at runtime  
> 🌈 Integrates with [`django`](https://docs.djangoproject.com/en/5.0/) >= 4.0, [`django-ninja`](https://django-ninja.dev/), and OpenAPI + [`django-jsonform`](https://django-jsonform.readthedocs.io/) out-of-the-box  
> 🦄 Uses [`pyinfra`](https://github.com/pyinfra-dev/pyinfra) / [`ansible`](https://github.com/ansible/ansible) for the actual install operations whenever possible (with internal fallbacks)

<sub><i>Built by <a href="https://github.com/ArchiveBox">ArchiveBox</a> to install & auto-update our extractor dependencies at runtime (<code>chrome</code>, <code>wget</code>, <code>curl</code>, etc.) on `macOS`/`Linux`/`Docker`.</i></sub>

<br/>

> [!WARNING]
> This is `BETA` software, the API is mostly stable but there may be minor changes later on.


**Source Code**: [https://github.com/ArchiveBox/pydantic-pkgr/](https://github.com/ArchiveBox/pydantic-pkgr/)

**Documentation**: [https://github.com/ArchiveBox/pydantic-pkgr/blob/main/README.md](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/README.md)

<br/>

```python
from pydantic_pkgr import *

apt, brew, pip, npm, env = AptProvider(), BrewProvider(), PipProvider(), NpmProvider(), EnvProvider()

dependencies = [
    Binary(name='curl',       binproviders=[env, apt, brew]),
    Binary(name='wget',       binproviders=[env, apt, brew]),
    Binary(name='yt-dlp',     binproviders=[env, pip, apt, brew]),
    Binary(name='playwright', binproviders=[env, pip, npm]),
    Binary(name='puppeteer',  binproviders=[env, npm]),
]
for binary in dependencies:
    binary = binary.load_or_install()

    print(binary.abspath, binary.version, binary.binprovider, binary.is_valid, binary.sha256)
    # Path('/usr/bin/curl') SemVer('7.81.0') AptProvider() True abc134...

    binary.exec(cmd=['--version'])   # curl 7.81.0 (x86_64-apple-darwin23.0) libcurl/7.81.0 ...
```

```python
from pydantic import InstanceOf
from pydantic_pkgr import Binary, BinProvider, BrewProvider, EnvProvider

# you can also define binaries as classes, making them usable for type checking
class CurlBinary(Binary):
    name: str = 'curl'
    binproviders: List[InstanceOf[BinProvider]] = [BrewProvider(), EnvProvider()]

curl = CurlBinary().install()
assert isinstance(curl, CurlBinary)                                 # CurlBinary is a unique type you can use in annotations now
print(curl.abspath, curl.version, curl.binprovider, curl.is_valid)  # Path('/opt/homebrew/bin/curl') SemVer('8.4.0') BrewProvider() True
curl.exec(cmd=['--version'])                                        # curl 8.4.0 (x86_64-apple-darwin23.0) libcurl/8.4.0 ...
```

```python
from pydantic_pkgr import Binary, EnvProvider, PipProvider

# We also provide direct package manager (aka BinProvider) APIs
apt = AptProvider()
apt.install('wget')
print(apt.PATH, apt.get_abspaths('wget'), apt.get_version('wget'))

# even if packages are installed by tools we don't control (e.g. pyinfra/ansible/puppet/etc.)
from pyinfra.operations import apt
apt.packages(name="Install ffmpeg", packages=['ffmpeg'], _sudo=True)

# our Binary API provides a nice type-checkable, validated, serializable handle
ffmpeg = Binary(name='ffmpeg').load()
print(ffmpeg)                       # name=ffmpeg abspath=/usr/bin/ffmpeg version=3.3.0 is_valid=True ...
print(ffmpeg.loaded_abspaths)       # show all the ffmpeg binaries found in $PATH (in case theres more than one available)
print(ffmpeg.model_dump_json())     # ... everything can also be dumped/loaded as json
print(ffmpeg.model_json_schema())   # ... all types provide OpenAPI-ready JSON schemas
```

### Supported Package Managers

**So far it supports `installing`/`finding installed`/~~`updating`/`removing`~~ packages on `Linux`/`macOS` with:**

- `apt` (Ubuntu/Debian/etc.)
- `brew` (macOS/Linux)
- `pip` (Linux/macOS/Windows)
- `npm` (Linux/macOS/Windows)
- `env` (looks for existing version of binary in user's `$PATH` at runtime)
- `vendor` (you can bundle vendored copies of packages you depend on within your source)

*Planned:* `docker`, `cargo`, `nix`, `apk`, `go get`, `gem`, `pkg`, *and more using `ansible`/[`pyinfra`](https://github.com/pyinfra-dev/pyinfra)...*

---


## Usage

```bash
pip install pydantic-pkgr
```

### [`BinProvider`](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/pydantic_pkgr/binprovider.py#:~:text=class%20BinProvider)

**Implementations: `EnvProvider`, `AptProvider`, `BrewProvider`, `PipProvider`, `NpmProvider`**

This type represents a "provider of binaries", e.g. a package manager like `apt`/`pip`/`npm`, or `env` (which finds binaries in your `$PATH`).

`BinProvider`s implement the following interface:
* `.INSTALLER_BIN -> /opt/homebrew/bin/brew`  provider's pkg manager location
* `.PATH -> PATHStr('/opt/homebrew/bin:/usr/local/bin:...')`  where provider stores bins
* `get_packages(bin_name: str) -> InstallArgs(['curl', 'libcurl4', '...])` find pkg dependencies for a bin
- `install(bin_name: str)` install a bin using binprovider to install needed packages
- `load(bin_name: str)`  find an existing installed binary
- `load_or_install(bin_name: str)` `->` `Binary` find existing / install if needed
- `get_version(bin_name: str) -> SemVer('1.0.0')`  get currently installed version
- `get_abspath(bin_name: str) -> Path('/absolute/path/to/bin')` get installed bin abspath
* `get_abspaths(bin_name: str) -> [Path('/opt/homebrew/bin/curl'), Path('/other/paths/to/curl'), ...]` get all matching bins found
* `get_sha256(bin_name: str) -> str` get sha256 hash hexdigest of the binary


```python
import platform
from typing import List
from pydantic_pkgr import EnvProvider, PipProvider, AptProvider, BrewProvider

### Example: Finding an existing install of bash using the system $PATH environment
env = EnvProvider()
bash = env.load(bin_name='bash')      # Binary('bash', provider=env)
print(bash.abspath)                   # Path('/opt/homebrew/bin/bash')
print(bash.version)                   # SemVer('5.2.26')
bash.exec(['-c', 'echo hi'])          # hi

### Example: Installing curl using the apt package manager
apt = AptProvider()
curl = apt.install(bin_name='curl')   # Binary('curl', provider=apt)
print(curl.abspath)                   # Path('/usr/bin/curl')
print(curl.version)                   # SemVer('8.4.0')
print(curl.sha256)                    # 9fd780521c97365f94c90724d80a889097ae1eeb2ffce67b87869cb7e79688ec
curl.exec(['--version'])              # curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 ...

### Example: Finding/Installing django with pip (w/ customized binpath resolution behavior)
pip = PipProvider(
    abspath_handler={'*': lambda self, bin_name, **context: inspect.getfile(bin_name)},  # use python inspect to get path instead of os.which
)
django_bin = pip.load_or_install('django') # Binary('django', provider=pip)
print(django_bin.abspath)             # Path('/usr/lib/python3.10/site-packages/django/__init__.py')
print(django_bin.version)             # SemVer('5.0.2')
```

### [`Binary`](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/pydantic_pkgr/binary.py#:~:text=class%20Binary)

This type represents a single binary dependency aka a package (e.g. `wget`, `curl`, `ffmpeg`, etc.).  
It can define one or more `BinProvider`s that it supports, along with overrides to customize the behavior for each.

`Binary`s implement the following interface:
- `load()`, `install()`, `load_or_install()` `->` `Binary`
- `binprovider: InstanceOf[BinProvider]`
- `abspath: Path`
- `abspaths: List[Path]`
- `version: SemVer`
- `sha256: str`

```python
from pydantic_pkgr import BinProvider, Binary, BinProviderName, BinName, ProviderLookupDict, SemVer

class CustomBrewProvider(BrewProvider):
    name: str = 'custom_brew'

    def get_macos_packages(self, bin_name: str, **context) -> List[str]:
        extra_packages_lookup_table = json.load(Path('macos_packages.json'))
        return extra_packages_lookup_table.get(platform.machine(), [bin_name])


### Example: Create a re-usable class defining a binary and its providers
class YtdlpBinary(Binary):
    name: BinName = 'ytdlp'
    description: str = 'YT-DLP (Replacement for YouTube-DL) Media Downloader'

    binproviders_supported: List[BinProvider] = [EnvProvider(), PipProvider(), AptProvider(), CustomBrewProvider()]
    
    # customize installed package names for specific package managers
    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
        'pip': {'packages': ['yt-dlp[default,curl-cffi]']}, # can use literal values (packages -> List[str], version -> SemVer, abspath -> Path, install -> str log)
        'apt': {'packages': lambda: ['yt-dlp', 'ffmpeg']},  # also accepts any pure Callable that returns a list of packages
        'brew': {'packages': 'self.get_macos_packages'},    # also accepts string reference to function on self (where self is the BinProvider)
    }


ytdlp = YtdlpBinary().load_or_install()
print(ytdlp.binprovider)                  # BrewProvider(...)
print(ytdlp.abspath)                      # Path('/opt/homebrew/bin/yt-dlp')
print(ytdlp.abspaths)                     # [Path('/opt/homebrew/bin/yt-dlp'), Path('/usr/local/bin/yt-dlp')]
print(ytdlp.version)                      # SemVer('2024.4.9')
print(ytdlp.sha256)                       # 46c3518cfa788090c42e379971485f56d007a6ce366dafb0556134ca724d6a36
print(ytdlp.is_valid)                     # True
```

```python
from pydantic_pkgr import BinProvider, Binary, BinProviderName, BinName, ProviderLookupDict, SemVer

#### Example: Create a binary that uses Podman if available, or Docker otherwise
class DockerBinary(Binary):
    name: BinName = 'docker'

    binproviders_supported: List[BinProvider] = [EnvProvider(), AptProvider()]
    
    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
        'env': {
            # example: prefer podman if installed (falling back to docker)
            'abspath': lambda: os.which('podman') or os.which('docker') or os.which('docker-ce'),
        },
        'apt': {
            # example: vary installed package name based on your CPU architecture
            'packages': {
                'amd64': ['docker'],
                'armv7l': ['docker-ce'],
                'arm64': ['docker-ce'],
            }.get(platform.machine(), 'docker'),
        },
    }

docker = DockerBinary().load_or_install()
print(docker.binprovider)                 # EnvProvider()
print(docker.abspath)                     # Path('/usr/local/bin/podman')
print(docker.abspaths)                    # [Path('/usr/local/bin/podman'), Path('/opt/homebrew/bin/podman')]
print(docker.version)                     # SemVer('6.0.2')
print(docker.is_valid)                    # True

# You can also pass **kwargs to override properties at runtime,
# e.g. if you want to force the abspath to be at a specific path:
custom_docker = DockerBinary(abspath='~/custom/bin/podman').load()
print(custom_docker.name)                 # 'docker'
print(custom_docker.binprovider)          # EnvProvider()
print(custom_docker.abspath)              # Path('/Users/example/custom/bin/podman')
print(custom_docker.version)              # SemVer('5.0.2')
print(custom_docker.is_valid)             # True
```

### [`SemVer`](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/pydantic_pkgr/semver.py#:~:text=class%20SemVer)

```python
from pydantic_pkgr import SemVer

### Example: Use the SemVer type directly for parsing & verifying version strings
SemVer.parse('Google Chrome 124.0.6367.208+beta_234. 234.234.123')  # SemVer(124, 0, 6367')
SemVer.parse('2024.04.05)                                           # SemVer(2024, 4, 5)
SemVer.parse('1.9+beta')                                            # SemVer(1, 9, 0)
str(SemVer(1, 9, 0))                                                # '1.9.0'
```
<br/>

> These types are all meant to be used library-style to make writing your own apps easier.  
> e.g. you can use it to build things like: [`playwright install --with-deps`](https://playwright.dev/docs/browsers#install-system-dependencies))


<br/>

---

<br/>


## Django Usage

The pydantic ecosystem helps us get auto-generated, type-checked Django fields & forms that support `BinProvider` and `Binary`.

> [!TIP]
> For the full Django experience, we recommend installing these 3 excellent packages:
> - [`django-admin-data-views`](https://github.com/MrThearMan/django-admin-data-views)
> - [`django-pydantic-field`](https://github.com/surenkov/django-pydantic-field)
> - [`django-jsonform`](https://django-jsonform.readthedocs.io/)  
> `pip install pydantic-pkgr django-admin-data-views django-pydantic-field django-jsonform`

<br/>

### Django Model Usage: Store `BinProvider` and `Binary` entries in your model fields

```bash
pip install django-pydantic-field
```

*Fore more info see the [`django-pydantic-field`](https://github.com/surenkov/django-pydantic-field) docs...*

Example Django `models.py` showing how to store `Binary` and `BinProvider` instances in DB fields:
```python
from typing import List
from django.db import models
from pydantic import InstanceOf
from pydantic_pkgr import BinProvider, Binary, SemVer
from django_pydantic_field import SchemaField

class InstalledBinary(models.Model):
    name = models.CharField(max_length=63)
    binary: Binary = SchemaField()
    binproviders: List[InstanceOf[BinProvider]] = SchemaField(default=[])
    version: SemVer = SchemaField(default=(0,0,1))
```

And here's how to save a `Binary` using the example model:
```python
# find existing curl Binary in $PATH
curl = Binary(name='curl').load()

# save it to the DB using our new model
obj = InstalledBinary(
    name='curl',
    binary=curl,                                  # store Binary/BinProvider/SemVer values directly in fields
    binproviders=[env],                           # no need for manual JSON serialization / schema checking
    min_version=SemVer('6.5.0'),
)
obj.save()                                      
```

When fetching it back from the DB, the `Binary` field is auto-deserialized / immediately usable:
```
obj = InstalledBinary.objects.get(name='curl')    # everything is transparently serialized to/from the DB,
                                                  # and is ready to go immediately after querying:
assert obj.binary.abspath == curl.abspath
print(obj.binary.abspath)                         #   Path('/usr/local/bin/curl')
obj.binary.exec(['--version'])                    #   curl 7.81.0 (x86_64-apple-darwin23.0) libcurl/7.81.0 ...
```
*For a full example see our provided [`django_example_project/`](https://github.com/ArchiveBox/pydantic-pkgr/tree/main/django_example_project)...*

<br/>

### Django Admin Usage: Display `Binary` objects nicely in the Admin UI

<img height="220" alt="Django Admin binaries list view" src="https://github.com/ArchiveBox/pydantic-pkgr/assets/511499/a9980217-f39e-434e-b266-20cd6feb17c3" align="top"><img height="220" alt="Django Admin binaries detail view" src="https://github.com/ArchiveBox/pydantic-pkgr/assets/511499/d4d9086e-c8f4-4b6e-8ee8-8c8a864715b0" align="top">

```bash
pip install pydantic-pkgr django-admin-data-views
```
*For more info see the [`django-admin-data-views`](https://github.com/MrThearMan/django-admin-data-views) docs...*

Then add this to your `settings.py`:
```python
INSTALLED_APPS = [
    # ...
    'admin_data_views'
    'pydantic_pkgr'
    # ...
]

# point these to a function that gets the list of all binaries / a single binary
PYDANTIC_PKGR_GET_ALL_BINARIES = 'pydantic_pkgr.views.get_all_binaries'
PYDANTIC_PKGR_GET_BINARY = 'pydantic_pkgr.views.get_binary'

ADMIN_DATA_VIEWS = {
    "NAME": "Environment",
    "URLS": [
        {
            "route": "binaries/",
            "view": "pydantic_pkgr.views.binaries_list_view",
            "name": "binaries",
            "items": {
                "route": "<str:key>/",
                "view": "pydantic_pkgr.views.binary_detail_view",
                "name": "binary",
            },
        },
        # Coming soon: binprovider_list_view + binprovider_detail_view ...
    ],
}
```
*For a full example see our provided [`django_example_project/`](https://github.com/ArchiveBox/pydantic-pkgr/tree/main/django_example_project)...*

<details>
<summary><i>Note: If you override the default site admin, you must register the views manually...</i></summary>
<br/><br/>
<b><code>admin.py</code>:</b>
<br/>
<pre><code>
class YourSiteAdmin(admin.AdminSite):
    """Your customized version of admin.AdminSite"""
    ...
<br/>
custom_admin = YourSiteAdmin()
custom_admin.register(get_user_model())
...
from pydantic_pkgr.admin import register_admin_views
register_admin_views(custom_admin)
</code></pre>
</details>

<br/>

### ~~Django Admin Usage: JSONFormWidget for editing `BinProvider` and `Binary` data~~

<img src="https://github.com/ArchiveBox/pydantic-pkgr/assets/511499/63705a57-4f62-4dbe-9f3a-0515323d8b5e" width="600px"/>

> [!IMPORTANT]
> This feature is coming soon but is blocked on a few issues being fixed first:  
> - https://github.com/surenkov/django-pydantic-field/issues/64
> - https://github.com/surenkov/django-pydantic-field/issues/65
> - https://github.com/surenkov/django-pydantic-field/issues/66

<details><summary>Expand to see more...</summary>

~~Install `django-jsonform` to get auto-generated Forms for editing BinProvider, Binary, etc. data~~
```bash
pip install django-pydantic-field django-jsonform
```
*For more info see the [`django-jsonform`](https://django-jsonform.readthedocs.io/) docs...*

`admin.py`:
```python
from django.contrib import admin
from django_jsonform.widgets import JSONFormWidget
from django_pydantic_field.v2.fields import PydanticSchemaField

class MyModelAdmin(admin.ModelAdmin):
    formfield_overrides = {PydanticSchemaField: {"widget": JSONFormWidget}}

admin.site.register(MyModel, MyModelAdmin)
```

</details>

*For a full example see our provided [`django_example_project/`](https://github.com/ArchiveBox/pydantic-pkgr/tree/main/django_example_project)...*

<br/>

---

<br/>


## Examples

### Advanced: Implement your own package manager behavior by subclassing BinProvider

```python
from subprocess import run, PIPE

from pydantic_pkgr import BinProvider, BinProviderName, BinName, SemVer

class CargoProvider(BinProvider):
    name: BinProviderName = 'cargo'
    
    def on_setup_paths(self):
        if '~/.cargo/bin' not in sys.path:
            sys.path.append('~/.cargo/bin')

    def on_install(self, bin_name: BinName, **context):
        packages = self.on_get_packages(bin_name)
        installer_process = run(['cargo', 'install', *packages.split(' ')], capture_output = True, text=True)
        assert installer_process.returncode == 0

    def on_get_packages(self, bin_name: BinName, **context) -> InstallArgs:
        # optionally remap bin_names to strings passed to installer 
        # e.g. 'yt-dlp' -> ['yt-dlp, 'ffmpeg', 'libcffi', 'libaac']
        return [bin_name]

    def on_get_abspath(self, bin_name: BinName, **context) -> Path | None:
        self.on_setup_paths()
        return Path(os.which(bin_name))

    def on_get_version(self, bin_name: BinName, **context) -> SemVer | None:
        self.on_setup_paths()
        return SemVer(run([bin_name, '--version'], stdout=PIPE).stdout.decode())

cargo = CargoProvider()
rg = cargo.install(bin_name='ripgrep')
print(rg.binprovider)                   # CargoProvider()
print(rg.version)                       # SemVer(14, 1, 0)
```


<br/>

---

<br/>

### TODO

- [x] Implement initial basic support for `apt`, `brew`, and `pip`
- [x] Provide editability and actions via Django Admin UI using [`django-pydantic-field`](https://github.com/surenkov/django-pydantic-field) and [`django-jsonform`](https://django-jsonform.readthedocs.io/en/latest/)
- [ ] Implement `update` and `remove` actions on BinProviders
- [ ] Add `preinstall` and `postinstall` hooks for things like adding `apt` sources and running cleanup scripts
- [ ] Implement more package managers (`cargo`, `gem`, `go get`, `ppm`, `nix`, `docker`, etc.)
- [ ] Add `Binary.min_version` that affects `.is_valid` based on whether it meets minimum `SemVer` threshold


### Other Packages We Like

- https://github.com/MrThearMan/django-signal-webhooks
- https://github.com/MrThearMan/django-admin-data-views
- https://github.com/lazybird/django-solo
- https://github.com/joshourisman/django-pydantic-settings
- https://github.com/surenkov/django-pydantic-field
- https://github.com/jordaneremieff/djantic

[coverage-badge]: https://coveralls.io/repos/github/ArchiveBox/pydantic-pkgr/badge.svg?branch=main
[status-badge]: https://img.shields.io/github/actions/workflow/status/ArchiveBox/pydantic-pkgr/test.yml?branch=main
[pypi-badge]: https://img.shields.io/pypi/v/pydantic-pkgr?v=1
[licence-badge]: https://img.shields.io/github/license/ArchiveBox/pydantic-pkgr?v=1
[repo-badge]: https://img.shields.io/github/last-commit/ArchiveBox/pydantic-pkgr?v=1
[issues-badge]: https://img.shields.io/github/issues-raw/ArchiveBox/pydantic-pkgr?v=1
[version-badge]: https://img.shields.io/pypi/pyversions/pydantic-pkgr?v=1
[downloads-badge]: https://img.shields.io/pypi/dm/pydantic-pkgr?v=1
[django-badge]: https://img.shields.io/pypi/djversions/pydantic-pkgr?v=1

[coverage]: https://coveralls.io/github/ArchiveBox/pydantic-pkgr?branch=main
[status]: https://github.com/ArchiveBox/pydantic-pkgr/actions/workflows/test.yml
[pypi]: https://pypi.org/project/pydantic-pkgr
[licence]: https://github.com/ArchiveBox/pydantic-pkgr/blob/main/LICENSE
[repo]: https://github.com/ArchiveBox/pydantic-pkgr/commits/main
[issues]: https://github.com/ArchiveBox/pydantic-pkgr/issues

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "pydantic-pkgr",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "pydantic, packagemanager, apt, brew, pip, system, dependencies",
    "author": null,
    "author_email": "Nick Sweeting <pydantic-pkgr-pyproject-toml@sweeting.me>",
    "download_url": "https://files.pythonhosted.org/packages/d2/18/3bf29e213c4a19d5b08e0fa1048c72f76c54565a208cced1fd4a60f989fc/pydantic_pkgr-0.5.4.tar.gz",
    "platform": null,
    "description": "<h1><a href=\"https://github.com/ArchiveBox/pydantic-pkgr\"><code>pydantic-pkgr</code></a> &nbsp; &nbsp; &nbsp; &nbsp; \ud83d\udce6  <small><code>apt</code>&nbsp; <code>brew</code>&nbsp; <code>pip</code>&nbsp; <code>npm</code> &nbsp;\u208a\u208a\u208a</small><br/><sub>Simple Pydantic interfaces for package managers + installed binaries.</sub></h1>\n<br/>\n\n[![PyPI][pypi-badge]][pypi]\n[![Python Version][version-badge]][pypi]\n[![Django Version][django-badge]][pypi]\n[![GitHub][licence-badge]][licence]\n[![GitHub Last Commit][repo-badge]][repo]\n<!--[![Downloads][downloads-badge]][pypi]-->\n\n<br/>\n\n**It's an ORM for your package managers, providing a nice python types for packages + installers.**  \n  \n**This is a [Python library](https://pypi.org/project/pydantic-pkgr/) for installing & managing packages locally with a variety of package managers.**  \nIt's designed for when `pip` dependencies aren't enough, and your app has to check for & install dependencies at runtime.  \n\n\n```shell\npip install pydantic-pkgr\n```\n\n\n> \u2728 Built with [`pydantic`](https://pydantic-docs.helpmanual.io/) v2 for strong static typing guarantees and json import/export compatibility  \n> \ud83d\udce6 Provides consistent cross-platform interfaces for dependency resolution & installation at runtime  \n> \ud83c\udf08 Integrates with [`django`](https://docs.djangoproject.com/en/5.0/) >= 4.0, [`django-ninja`](https://django-ninja.dev/), and OpenAPI + [`django-jsonform`](https://django-jsonform.readthedocs.io/) out-of-the-box  \n> \ud83e\udd84 Uses [`pyinfra`](https://github.com/pyinfra-dev/pyinfra) / [`ansible`](https://github.com/ansible/ansible) for the actual install operations whenever possible (with internal fallbacks)\n\n<sub><i>Built by <a href=\"https://github.com/ArchiveBox\">ArchiveBox</a> to install & auto-update our extractor dependencies at runtime (<code>chrome</code>, <code>wget</code>, <code>curl</code>, etc.) on `macOS`/`Linux`/`Docker`.</i></sub>\n\n<br/>\n\n> [!WARNING]\n> This is `BETA` software, the API is mostly stable but there may be minor changes later on.\n\n\n**Source Code**: [https://github.com/ArchiveBox/pydantic-pkgr/](https://github.com/ArchiveBox/pydantic-pkgr/)\n\n**Documentation**: [https://github.com/ArchiveBox/pydantic-pkgr/blob/main/README.md](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/README.md)\n\n<br/>\n\n```python\nfrom pydantic_pkgr import *\n\napt, brew, pip, npm, env = AptProvider(), BrewProvider(), PipProvider(), NpmProvider(), EnvProvider()\n\ndependencies = [\n    Binary(name='curl',       binproviders=[env, apt, brew]),\n    Binary(name='wget',       binproviders=[env, apt, brew]),\n    Binary(name='yt-dlp',     binproviders=[env, pip, apt, brew]),\n    Binary(name='playwright', binproviders=[env, pip, npm]),\n    Binary(name='puppeteer',  binproviders=[env, npm]),\n]\nfor binary in dependencies:\n    binary = binary.load_or_install()\n\n    print(binary.abspath, binary.version, binary.binprovider, binary.is_valid, binary.sha256)\n    # Path('/usr/bin/curl') SemVer('7.81.0') AptProvider() True abc134...\n\n    binary.exec(cmd=['--version'])   # curl 7.81.0 (x86_64-apple-darwin23.0) libcurl/7.81.0 ...\n```\n\n```python\nfrom pydantic import InstanceOf\nfrom pydantic_pkgr import Binary, BinProvider, BrewProvider, EnvProvider\n\n# you can also define binaries as classes, making them usable for type checking\nclass CurlBinary(Binary):\n    name: str = 'curl'\n    binproviders: List[InstanceOf[BinProvider]] = [BrewProvider(), EnvProvider()]\n\ncurl = CurlBinary().install()\nassert isinstance(curl, CurlBinary)                                 # CurlBinary is a unique type you can use in annotations now\nprint(curl.abspath, curl.version, curl.binprovider, curl.is_valid)  # Path('/opt/homebrew/bin/curl') SemVer('8.4.0') BrewProvider() True\ncurl.exec(cmd=['--version'])                                        # curl 8.4.0 (x86_64-apple-darwin23.0) libcurl/8.4.0 ...\n```\n\n```python\nfrom pydantic_pkgr import Binary, EnvProvider, PipProvider\n\n# We also provide direct package manager (aka BinProvider) APIs\napt = AptProvider()\napt.install('wget')\nprint(apt.PATH, apt.get_abspaths('wget'), apt.get_version('wget'))\n\n# even if packages are installed by tools we don't control (e.g. pyinfra/ansible/puppet/etc.)\nfrom pyinfra.operations import apt\napt.packages(name=\"Install ffmpeg\", packages=['ffmpeg'], _sudo=True)\n\n# our Binary API provides a nice type-checkable, validated, serializable handle\nffmpeg = Binary(name='ffmpeg').load()\nprint(ffmpeg)                       # name=ffmpeg abspath=/usr/bin/ffmpeg version=3.3.0 is_valid=True ...\nprint(ffmpeg.loaded_abspaths)       # show all the ffmpeg binaries found in $PATH (in case theres more than one available)\nprint(ffmpeg.model_dump_json())     # ... everything can also be dumped/loaded as json\nprint(ffmpeg.model_json_schema())   # ... all types provide OpenAPI-ready JSON schemas\n```\n\n### Supported Package Managers\n\n**So far it supports `installing`/`finding installed`/~~`updating`/`removing`~~ packages on `Linux`/`macOS` with:**\n\n- `apt` (Ubuntu/Debian/etc.)\n- `brew` (macOS/Linux)\n- `pip` (Linux/macOS/Windows)\n- `npm` (Linux/macOS/Windows)\n- `env` (looks for existing version of binary in user's `$PATH` at runtime)\n- `vendor` (you can bundle vendored copies of packages you depend on within your source)\n\n*Planned:* `docker`, `cargo`, `nix`, `apk`, `go get`, `gem`, `pkg`, *and more using `ansible`/[`pyinfra`](https://github.com/pyinfra-dev/pyinfra)...*\n\n---\n\n\n## Usage\n\n```bash\npip install pydantic-pkgr\n```\n\n### [`BinProvider`](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/pydantic_pkgr/binprovider.py#:~:text=class%20BinProvider)\n\n**Implementations: `EnvProvider`, `AptProvider`, `BrewProvider`, `PipProvider`, `NpmProvider`**\n\nThis type represents a \"provider of binaries\", e.g. a package manager like `apt`/`pip`/`npm`, or `env` (which finds binaries in your `$PATH`).\n\n`BinProvider`s implement the following interface:\n* `.INSTALLER_BIN -> /opt/homebrew/bin/brew`  provider's pkg manager location\n* `.PATH -> PATHStr('/opt/homebrew/bin:/usr/local/bin:...')`  where provider stores bins\n* `get_packages(bin_name: str) -> InstallArgs(['curl', 'libcurl4', '...])` find pkg dependencies for a bin\n- `install(bin_name: str)` install a bin using binprovider to install needed packages\n- `load(bin_name: str)`  find an existing installed binary\n- `load_or_install(bin_name: str)` `->` `Binary` find existing / install if needed\n- `get_version(bin_name: str) -> SemVer('1.0.0')`  get currently installed version\n- `get_abspath(bin_name: str) -> Path('/absolute/path/to/bin')` get installed bin abspath\n* `get_abspaths(bin_name: str) -> [Path('/opt/homebrew/bin/curl'), Path('/other/paths/to/curl'), ...]` get all matching bins found\n* `get_sha256(bin_name: str) -> str` get sha256 hash hexdigest of the binary\n\n\n```python\nimport platform\nfrom typing import List\nfrom pydantic_pkgr import EnvProvider, PipProvider, AptProvider, BrewProvider\n\n### Example: Finding an existing install of bash using the system $PATH environment\nenv = EnvProvider()\nbash = env.load(bin_name='bash')      # Binary('bash', provider=env)\nprint(bash.abspath)                   # Path('/opt/homebrew/bin/bash')\nprint(bash.version)                   # SemVer('5.2.26')\nbash.exec(['-c', 'echo hi'])          # hi\n\n### Example: Installing curl using the apt package manager\napt = AptProvider()\ncurl = apt.install(bin_name='curl')   # Binary('curl', provider=apt)\nprint(curl.abspath)                   # Path('/usr/bin/curl')\nprint(curl.version)                   # SemVer('8.4.0')\nprint(curl.sha256)                    # 9fd780521c97365f94c90724d80a889097ae1eeb2ffce67b87869cb7e79688ec\ncurl.exec(['--version'])              # curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 ...\n\n### Example: Finding/Installing django with pip (w/ customized binpath resolution behavior)\npip = PipProvider(\n    abspath_handler={'*': lambda self, bin_name, **context: inspect.getfile(bin_name)},  # use python inspect to get path instead of os.which\n)\ndjango_bin = pip.load_or_install('django') # Binary('django', provider=pip)\nprint(django_bin.abspath)             # Path('/usr/lib/python3.10/site-packages/django/__init__.py')\nprint(django_bin.version)             # SemVer('5.0.2')\n```\n\n### [`Binary`](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/pydantic_pkgr/binary.py#:~:text=class%20Binary)\n\nThis type represents a single binary dependency aka a package (e.g. `wget`, `curl`, `ffmpeg`, etc.).  \nIt can define one or more `BinProvider`s that it supports, along with overrides to customize the behavior for each.\n\n`Binary`s implement the following interface:\n- `load()`, `install()`, `load_or_install()` `->` `Binary`\n- `binprovider: InstanceOf[BinProvider]`\n- `abspath: Path`\n- `abspaths: List[Path]`\n- `version: SemVer`\n- `sha256: str`\n\n```python\nfrom pydantic_pkgr import BinProvider, Binary, BinProviderName, BinName, ProviderLookupDict, SemVer\n\nclass CustomBrewProvider(BrewProvider):\n    name: str = 'custom_brew'\n\n    def get_macos_packages(self, bin_name: str, **context) -> List[str]:\n        extra_packages_lookup_table = json.load(Path('macos_packages.json'))\n        return extra_packages_lookup_table.get(platform.machine(), [bin_name])\n\n\n### Example: Create a re-usable class defining a binary and its providers\nclass YtdlpBinary(Binary):\n    name: BinName = 'ytdlp'\n    description: str = 'YT-DLP (Replacement for YouTube-DL) Media Downloader'\n\n    binproviders_supported: List[BinProvider] = [EnvProvider(), PipProvider(), AptProvider(), CustomBrewProvider()]\n    \n    # customize installed package names for specific package managers\n    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {\n        'pip': {'packages': ['yt-dlp[default,curl-cffi]']}, # can use literal values (packages -> List[str], version -> SemVer, abspath -> Path, install -> str log)\n        'apt': {'packages': lambda: ['yt-dlp', 'ffmpeg']},  # also accepts any pure Callable that returns a list of packages\n        'brew': {'packages': 'self.get_macos_packages'},    # also accepts string reference to function on self (where self is the BinProvider)\n    }\n\n\nytdlp = YtdlpBinary().load_or_install()\nprint(ytdlp.binprovider)                  # BrewProvider(...)\nprint(ytdlp.abspath)                      # Path('/opt/homebrew/bin/yt-dlp')\nprint(ytdlp.abspaths)                     # [Path('/opt/homebrew/bin/yt-dlp'), Path('/usr/local/bin/yt-dlp')]\nprint(ytdlp.version)                      # SemVer('2024.4.9')\nprint(ytdlp.sha256)                       # 46c3518cfa788090c42e379971485f56d007a6ce366dafb0556134ca724d6a36\nprint(ytdlp.is_valid)                     # True\n```\n\n```python\nfrom pydantic_pkgr import BinProvider, Binary, BinProviderName, BinName, ProviderLookupDict, SemVer\n\n#### Example: Create a binary that uses Podman if available, or Docker otherwise\nclass DockerBinary(Binary):\n    name: BinName = 'docker'\n\n    binproviders_supported: List[BinProvider] = [EnvProvider(), AptProvider()]\n    \n    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {\n        'env': {\n            # example: prefer podman if installed (falling back to docker)\n            'abspath': lambda: os.which('podman') or os.which('docker') or os.which('docker-ce'),\n        },\n        'apt': {\n            # example: vary installed package name based on your CPU architecture\n            'packages': {\n                'amd64': ['docker'],\n                'armv7l': ['docker-ce'],\n                'arm64': ['docker-ce'],\n            }.get(platform.machine(), 'docker'),\n        },\n    }\n\ndocker = DockerBinary().load_or_install()\nprint(docker.binprovider)                 # EnvProvider()\nprint(docker.abspath)                     # Path('/usr/local/bin/podman')\nprint(docker.abspaths)                    # [Path('/usr/local/bin/podman'), Path('/opt/homebrew/bin/podman')]\nprint(docker.version)                     # SemVer('6.0.2')\nprint(docker.is_valid)                    # True\n\n# You can also pass **kwargs to override properties at runtime,\n# e.g. if you want to force the abspath to be at a specific path:\ncustom_docker = DockerBinary(abspath='~/custom/bin/podman').load()\nprint(custom_docker.name)                 # 'docker'\nprint(custom_docker.binprovider)          # EnvProvider()\nprint(custom_docker.abspath)              # Path('/Users/example/custom/bin/podman')\nprint(custom_docker.version)              # SemVer('5.0.2')\nprint(custom_docker.is_valid)             # True\n```\n\n### [`SemVer`](https://github.com/ArchiveBox/pydantic-pkgr/blob/main/pydantic_pkgr/semver.py#:~:text=class%20SemVer)\n\n```python\nfrom pydantic_pkgr import SemVer\n\n### Example: Use the SemVer type directly for parsing & verifying version strings\nSemVer.parse('Google Chrome 124.0.6367.208+beta_234. 234.234.123')  # SemVer(124, 0, 6367')\nSemVer.parse('2024.04.05)                                           # SemVer(2024, 4, 5)\nSemVer.parse('1.9+beta')                                            # SemVer(1, 9, 0)\nstr(SemVer(1, 9, 0))                                                # '1.9.0'\n```\n<br/>\n\n> These types are all meant to be used library-style to make writing your own apps easier.  \n> e.g. you can use it to build things like: [`playwright install --with-deps`](https://playwright.dev/docs/browsers#install-system-dependencies))\n\n\n<br/>\n\n---\n\n<br/>\n\n\n## Django Usage\n\nThe pydantic ecosystem helps us get auto-generated, type-checked Django fields & forms that support `BinProvider` and `Binary`.\n\n> [!TIP]\n> For the full Django experience, we recommend installing these 3 excellent packages:\n> - [`django-admin-data-views`](https://github.com/MrThearMan/django-admin-data-views)\n> - [`django-pydantic-field`](https://github.com/surenkov/django-pydantic-field)\n> - [`django-jsonform`](https://django-jsonform.readthedocs.io/)  \n> `pip install pydantic-pkgr django-admin-data-views django-pydantic-field django-jsonform`\n\n<br/>\n\n### Django Model Usage: Store `BinProvider` and `Binary` entries in your model fields\n\n```bash\npip install django-pydantic-field\n```\n\n*Fore more info see the [`django-pydantic-field`](https://github.com/surenkov/django-pydantic-field) docs...*\n\nExample Django `models.py` showing how to store `Binary` and `BinProvider` instances in DB fields:\n```python\nfrom typing import List\nfrom django.db import models\nfrom pydantic import InstanceOf\nfrom pydantic_pkgr import BinProvider, Binary, SemVer\nfrom django_pydantic_field import SchemaField\n\nclass InstalledBinary(models.Model):\n    name = models.CharField(max_length=63)\n    binary: Binary = SchemaField()\n    binproviders: List[InstanceOf[BinProvider]] = SchemaField(default=[])\n    version: SemVer = SchemaField(default=(0,0,1))\n```\n\nAnd here's how to save a `Binary` using the example model:\n```python\n# find existing curl Binary in $PATH\ncurl = Binary(name='curl').load()\n\n# save it to the DB using our new model\nobj = InstalledBinary(\n    name='curl',\n    binary=curl,                                  # store Binary/BinProvider/SemVer values directly in fields\n    binproviders=[env],                           # no need for manual JSON serialization / schema checking\n    min_version=SemVer('6.5.0'),\n)\nobj.save()                                      \n```\n\nWhen fetching it back from the DB, the `Binary` field is auto-deserialized / immediately usable:\n```\nobj = InstalledBinary.objects.get(name='curl')    # everything is transparently serialized to/from the DB,\n                                                  # and is ready to go immediately after querying:\nassert obj.binary.abspath == curl.abspath\nprint(obj.binary.abspath)                         #   Path('/usr/local/bin/curl')\nobj.binary.exec(['--version'])                    #   curl 7.81.0 (x86_64-apple-darwin23.0) libcurl/7.81.0 ...\n```\n*For a full example see our provided [`django_example_project/`](https://github.com/ArchiveBox/pydantic-pkgr/tree/main/django_example_project)...*\n\n<br/>\n\n### Django Admin Usage: Display `Binary` objects nicely in the Admin UI\n\n<img height=\"220\" alt=\"Django Admin binaries list view\" src=\"https://github.com/ArchiveBox/pydantic-pkgr/assets/511499/a9980217-f39e-434e-b266-20cd6feb17c3\" align=\"top\"><img height=\"220\" alt=\"Django Admin binaries detail view\" src=\"https://github.com/ArchiveBox/pydantic-pkgr/assets/511499/d4d9086e-c8f4-4b6e-8ee8-8c8a864715b0\" align=\"top\">\n\n```bash\npip install pydantic-pkgr django-admin-data-views\n```\n*For more info see the [`django-admin-data-views`](https://github.com/MrThearMan/django-admin-data-views) docs...*\n\nThen add this to your `settings.py`:\n```python\nINSTALLED_APPS = [\n    # ...\n    'admin_data_views'\n    'pydantic_pkgr'\n    # ...\n]\n\n# point these to a function that gets the list of all binaries / a single binary\nPYDANTIC_PKGR_GET_ALL_BINARIES = 'pydantic_pkgr.views.get_all_binaries'\nPYDANTIC_PKGR_GET_BINARY = 'pydantic_pkgr.views.get_binary'\n\nADMIN_DATA_VIEWS = {\n    \"NAME\": \"Environment\",\n    \"URLS\": [\n        {\n            \"route\": \"binaries/\",\n            \"view\": \"pydantic_pkgr.views.binaries_list_view\",\n            \"name\": \"binaries\",\n            \"items\": {\n                \"route\": \"<str:key>/\",\n                \"view\": \"pydantic_pkgr.views.binary_detail_view\",\n                \"name\": \"binary\",\n            },\n        },\n        # Coming soon: binprovider_list_view + binprovider_detail_view ...\n    ],\n}\n```\n*For a full example see our provided [`django_example_project/`](https://github.com/ArchiveBox/pydantic-pkgr/tree/main/django_example_project)...*\n\n<details>\n<summary><i>Note: If you override the default site admin, you must register the views manually...</i></summary>\n<br/><br/>\n<b><code>admin.py</code>:</b>\n<br/>\n<pre><code>\nclass YourSiteAdmin(admin.AdminSite):\n    \"\"\"Your customized version of admin.AdminSite\"\"\"\n    ...\n<br/>\ncustom_admin = YourSiteAdmin()\ncustom_admin.register(get_user_model())\n...\nfrom pydantic_pkgr.admin import register_admin_views\nregister_admin_views(custom_admin)\n</code></pre>\n</details>\n\n<br/>\n\n### ~~Django Admin Usage: JSONFormWidget for editing `BinProvider` and `Binary` data~~\n\n<img src=\"https://github.com/ArchiveBox/pydantic-pkgr/assets/511499/63705a57-4f62-4dbe-9f3a-0515323d8b5e\" width=\"600px\"/>\n\n> [!IMPORTANT]\n> This feature is coming soon but is blocked on a few issues being fixed first:  \n> - https://github.com/surenkov/django-pydantic-field/issues/64\n> - https://github.com/surenkov/django-pydantic-field/issues/65\n> - https://github.com/surenkov/django-pydantic-field/issues/66\n\n<details><summary>Expand to see more...</summary>\n\n~~Install `django-jsonform` to get auto-generated Forms for editing BinProvider, Binary, etc. data~~\n```bash\npip install django-pydantic-field django-jsonform\n```\n*For more info see the [`django-jsonform`](https://django-jsonform.readthedocs.io/) docs...*\n\n`admin.py`:\n```python\nfrom django.contrib import admin\nfrom django_jsonform.widgets import JSONFormWidget\nfrom django_pydantic_field.v2.fields import PydanticSchemaField\n\nclass MyModelAdmin(admin.ModelAdmin):\n    formfield_overrides = {PydanticSchemaField: {\"widget\": JSONFormWidget}}\n\nadmin.site.register(MyModel, MyModelAdmin)\n```\n\n</details>\n\n*For a full example see our provided [`django_example_project/`](https://github.com/ArchiveBox/pydantic-pkgr/tree/main/django_example_project)...*\n\n<br/>\n\n---\n\n<br/>\n\n\n## Examples\n\n### Advanced: Implement your own package manager behavior by subclassing BinProvider\n\n```python\nfrom subprocess import run, PIPE\n\nfrom pydantic_pkgr import BinProvider, BinProviderName, BinName, SemVer\n\nclass CargoProvider(BinProvider):\n    name: BinProviderName = 'cargo'\n    \n    def on_setup_paths(self):\n        if '~/.cargo/bin' not in sys.path:\n            sys.path.append('~/.cargo/bin')\n\n    def on_install(self, bin_name: BinName, **context):\n        packages = self.on_get_packages(bin_name)\n        installer_process = run(['cargo', 'install', *packages.split(' ')], capture_output = True, text=True)\n        assert installer_process.returncode == 0\n\n    def on_get_packages(self, bin_name: BinName, **context) -> InstallArgs:\n        # optionally remap bin_names to strings passed to installer \n        # e.g. 'yt-dlp' -> ['yt-dlp, 'ffmpeg', 'libcffi', 'libaac']\n        return [bin_name]\n\n    def on_get_abspath(self, bin_name: BinName, **context) -> Path | None:\n        self.on_setup_paths()\n        return Path(os.which(bin_name))\n\n    def on_get_version(self, bin_name: BinName, **context) -> SemVer | None:\n        self.on_setup_paths()\n        return SemVer(run([bin_name, '--version'], stdout=PIPE).stdout.decode())\n\ncargo = CargoProvider()\nrg = cargo.install(bin_name='ripgrep')\nprint(rg.binprovider)                   # CargoProvider()\nprint(rg.version)                       # SemVer(14, 1, 0)\n```\n\n\n<br/>\n\n---\n\n<br/>\n\n### TODO\n\n- [x] Implement initial basic support for `apt`, `brew`, and `pip`\n- [x] Provide editability and actions via Django Admin UI using [`django-pydantic-field`](https://github.com/surenkov/django-pydantic-field) and [`django-jsonform`](https://django-jsonform.readthedocs.io/en/latest/)\n- [ ] Implement `update` and `remove` actions on BinProviders\n- [ ] Add `preinstall` and `postinstall` hooks for things like adding `apt` sources and running cleanup scripts\n- [ ] Implement more package managers (`cargo`, `gem`, `go get`, `ppm`, `nix`, `docker`, etc.)\n- [ ] Add `Binary.min_version` that affects `.is_valid` based on whether it meets minimum `SemVer` threshold\n\n\n### Other Packages We Like\n\n- https://github.com/MrThearMan/django-signal-webhooks\n- https://github.com/MrThearMan/django-admin-data-views\n- https://github.com/lazybird/django-solo\n- https://github.com/joshourisman/django-pydantic-settings\n- https://github.com/surenkov/django-pydantic-field\n- https://github.com/jordaneremieff/djantic\n\n[coverage-badge]: https://coveralls.io/repos/github/ArchiveBox/pydantic-pkgr/badge.svg?branch=main\n[status-badge]: https://img.shields.io/github/actions/workflow/status/ArchiveBox/pydantic-pkgr/test.yml?branch=main\n[pypi-badge]: https://img.shields.io/pypi/v/pydantic-pkgr?v=1\n[licence-badge]: https://img.shields.io/github/license/ArchiveBox/pydantic-pkgr?v=1\n[repo-badge]: https://img.shields.io/github/last-commit/ArchiveBox/pydantic-pkgr?v=1\n[issues-badge]: https://img.shields.io/github/issues-raw/ArchiveBox/pydantic-pkgr?v=1\n[version-badge]: https://img.shields.io/pypi/pyversions/pydantic-pkgr?v=1\n[downloads-badge]: https://img.shields.io/pypi/dm/pydantic-pkgr?v=1\n[django-badge]: https://img.shields.io/pypi/djversions/pydantic-pkgr?v=1\n\n[coverage]: https://coveralls.io/github/ArchiveBox/pydantic-pkgr?branch=main\n[status]: https://github.com/ArchiveBox/pydantic-pkgr/actions/workflows/test.yml\n[pypi]: https://pypi.org/project/pydantic-pkgr\n[licence]: https://github.com/ArchiveBox/pydantic-pkgr/blob/main/LICENSE\n[repo]: https://github.com/ArchiveBox/pydantic-pkgr/commits/main\n[issues]: https://github.com/ArchiveBox/pydantic-pkgr/issues\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "System package manager APIs in strongly typed Python",
    "version": "0.5.4",
    "project_urls": {
        "Bug tracker": "https://github.com/ArchiveBox/pydantic-pkgr/issues",
        "Changelog": "https://github.com/ArchiveBox/pydantic-pkgr/releases",
        "Documentation": "https://github.com/ArchiveBox/pydantic-pkgr",
        "Donate": "https://github.com/ArchiveBox/ArchiveBox/wiki/Donations",
        "Homepage": "https://github.com/ArchiveBox/pydantic-pkgr",
        "Source": "https://github.com/ArchiveBox/pydantic-pkgr"
    },
    "split_keywords": [
        "pydantic",
        " packagemanager",
        " apt",
        " brew",
        " pip",
        " system",
        " dependencies"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "01979ec8d45e4af1a3af7d0ca78e12bcb1d74a446399034cb1514aab2bac056e",
                "md5": "ba111c1eb7ee550863e4eb13cd1819bc",
                "sha256": "46ad1ad5954ee9c55b2c2f2c2be749a39992a89edde624454e63d8a7b550be8b"
            },
            "downloads": -1,
            "filename": "pydantic_pkgr-0.5.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "ba111c1eb7ee550863e4eb13cd1819bc",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 45061,
            "upload_time": "2024-10-21T10:43:58",
            "upload_time_iso_8601": "2024-10-21T10:43:58.013900Z",
            "url": "https://files.pythonhosted.org/packages/01/97/9ec8d45e4af1a3af7d0ca78e12bcb1d74a446399034cb1514aab2bac056e/pydantic_pkgr-0.5.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d2183bf29e213c4a19d5b08e0fa1048c72f76c54565a208cced1fd4a60f989fc",
                "md5": "46678d9b4f35cbd1cd613b55170cc99a",
                "sha256": "e3487b46357b1e1b729363385590355cfac261b18ed207f59e9b613c5a8d45b2"
            },
            "downloads": -1,
            "filename": "pydantic_pkgr-0.5.4.tar.gz",
            "has_sig": false,
            "md5_digest": "46678d9b4f35cbd1cd613b55170cc99a",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 42408,
            "upload_time": "2024-10-21T10:43:59",
            "upload_time_iso_8601": "2024-10-21T10:43:59.883285Z",
            "url": "https://files.pythonhosted.org/packages/d2/18/3bf29e213c4a19d5b08e0fa1048c72f76c54565a208cced1fd4a60f989fc/pydantic_pkgr-0.5.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-10-21 10:43:59",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "ArchiveBox",
    "github_project": "pydantic-pkgr",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "pydantic-pkgr"
}
        
Elapsed time: 0.40548s