summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorA. Wilcox <AWilcox@Wilcox-Tech.com>2018-05-23 22:18:34 -0500
committerA. Wilcox <AWilcox@Wilcox-Tech.com>2018-05-23 22:18:34 -0500
commit2a46aec465207280f31dbe8031f420e8b9851ece (patch)
tree6d5a7ddeab97d267098400a8599ecd15bd217540
parent20d58e4a5ce099fd3884cb5fa417c6c0b24607d4 (diff)
downloadapkkit-2a46aec465207280f31dbe8031f420e8b9851ece.tar.gz
apkkit-2a46aec465207280f31dbe8031f420e8b9851ece.tar.bz2
apkkit-2a46aec465207280f31dbe8031f420e8b9851ece.tar.xz
apkkit-2a46aec465207280f31dbe8031f420e8b9851ece.zip
Add APKINDEX reading support
-rw-r--r--apkkit/base/index.py147
-rw-r--r--apkkit/base/package.py103
-rw-r--r--setup.py9
3 files changed, 249 insertions, 10 deletions
diff --git a/apkkit/base/index.py b/apkkit/base/index.py
new file mode 100644
index 0000000..ee0af9f
--- /dev/null
+++ b/apkkit/base/index.py
@@ -0,0 +1,147 @@
+"""Contains the Index class and related helper classes and functions."""
+
+from io import BytesIO
+import logging
+import os
+import requests
+import tarfile
+
+from apkkit.base.package import Package
+
+
+INDEX_LOGGER = logging.getLogger(__name__)
+
+
+class Index:
+ """The base index class."""
+
+ def __init__(self, packages=None, description=None, url=None, **kwargs):
+ """Initialise an Index object.
+
+ :param list packages:
+ The packages available in the repository this index represents.
+
+ :param str description:
+ (Recommended) The description of the repository this index
+ represents. Typically, something like "system" or "user".
+
+ :param str url:
+ (Optional) The URL to download the index from. All other
+ parameters are ignored if this is specified.
+ """
+
+ if url is not None:
+ self._url = url
+ resp = requests.get(url)
+ if resp.status_code != 200:
+ INDEX_LOGGER.error("could not download %s: %d (%r)", url,
+ resp.status_code, resp.text)
+ else:
+ self._fill_from_index_file(BytesIO(resp.content))
+ else:
+ if packages is None:
+ raise ValueError("Packages are required.")
+ self._pkgs = packages
+ self._desc = description
+
+ if len(kwargs) > 0:
+ INDEX_LOGGER.warning("unknown kwargs in Index: %r", kwargs)
+
+ @property
+ def description(self):
+ """The description of this repository."""
+ return self._desc
+
+ @property
+ def packages(self):
+ """The packages available in this repository."""
+ return list(self._pkgs)
+
+ @property
+ def origins(self):
+ """The names of all unique origin packages available in this repo."""
+ return {pkg.origin for pkg in self._pkgs}
+
+ def __repr__(self):
+ if self._url is not None:
+ return 'Index(url="{url}")'.format(url=self._url)
+ return 'Index(packages=<{num} packages>, description="{desc}")'.format(
+ num=len(self._pkgs), desc=self._desc)
+
+ def to_raw(self):
+ """Serialises this repository information into the APKINDEX format.
+
+ :returns str: The APKINDEX for this package. Unicode str, ready to be
+ written to a file.
+ """
+ raise NotImplemented("Not yet.")
+
+ def _fill_from_index_file(self, buf):
+ """Fill this `Index` object from the APKINDEX in `buf`."""
+ if len(getattr(self, '_pkgs', list())) > 0:
+ raise Exception("Attempt to fill an already filled Index")
+
+ pkgs = list()
+ params = {}
+ param_map = {'P': 'name', 'V': 'version', 'A': 'arch', 'o': 'origin',
+ 'T': 'description', 'p': 'provides', 'i': 'install_if',
+ 'D': 'depends', 'U': 'url', 'I': 'size', 'r': 'replaces',
+ 'L': 'license', 'q': 'replaces_priority', 'c': 'commit',
+ 'm': 'maintainer', 't': 'builddate'}
+ list_keys = {'p', 'D', 'i', 'r'}
+
+ tar = None
+
+ # assumption: "P" is the first line of each package.
+ try:
+ tar = tarfile.open(fileobj=buf)
+ real_buf = tar.extractfile('APKINDEX')
+ except:
+ real_buf = buf
+
+ for line in real_buf.readlines():
+ if not isinstance(line, str):
+ line = line.decode('utf-8')
+
+ # Skip comments.
+ if line[0] == '#':
+ continue
+
+ # separated by blank line
+ if line == "\n" and params.get('name', None) is not None:
+ pkgs.append(Package(**params))
+ params.clear()
+ continue
+
+ if line.find(':') == -1:
+ INDEX_LOGGER.warning('!!! malformed line? "%s" !!!', line)
+ continue
+
+ (key, value) = line.split(':', 1)
+ key = key.strip()
+ value = value.strip()
+
+ if key in param_map:
+ if key in list_keys:
+ params[param_map[key]] = value.split(' ')
+ else:
+ params[param_map[key]] = value
+ else:
+ INDEX_LOGGER.info('!!! unrecognised APKINDEX key %s !!!', key)
+
+ self._pkgs = pkgs
+
+ @classmethod
+ def from_raw(cls, buf):
+ """Create a new :py:class:`Index` object from an existing APKINDEX.
+
+ :param buf:
+ The buffer to read from (whether file, BytesIO, etc).
+
+ :returns:
+ A :py:class:`Index` object with the details from the APKINDEX.
+ """
+ idx = cls(packages=list())
+ idx._fill_from_index_file(buf)
+ return idx
+
diff --git a/apkkit/base/package.py b/apkkit/base/package.py
index a8e1192..109cf04 100644
--- a/apkkit/base/package.py
+++ b/apkkit/base/package.py
@@ -3,6 +3,7 @@
from jinja2 import Template
import logging
import os
+import time
PACKAGE_LOGGER = logging.getLogger(__name__)
@@ -10,24 +11,43 @@ PACKAGE_LOGGER = logging.getLogger(__name__)
PKGINFO_TEMPLATE = Template("""
# Generated by APK Kit for Adélie Linux
-# {{ builduser }}@{{ buildhost }} {{ builddate }}
+# {{ builduser }}@{{ buildhost }} {{ self.builddate }}
pkgname = {{ package.name }}
pkgver = {{ package.version }}
pkgdesc = {{ package.description }}
arch = {{ package.arch }}
size = {{ package.size }}
+{%- if package.license %}
+license = {{ package.license }}
+{%- endif %}
{%- if package.url %}
url = {{ package.url }}
{%- endif %}
+{%- if package.origin %}
+origin = {{ package.origin }}
+{%- endif %}
{%- if package.provides %}{%- for provided in package.provides %}
provides = {{ provided }}
{%- endfor %}{%- endif %}
{%- if package.depends %}{%- for depend in package.depends %}
depend = {{ depend }}
{%- endfor %}{%- endif %}
+{%- if package.replaces %}{%- for replace in package.replaces %}
+replaces = {{ replace }}
+{%- endfor %}{%- endif %}
+{%- if package.install_if %}{%- for iif in package.install_if %}
+install_if = {{ iif }}
+{%- endfor %}{%- endif %}
+builddate = {{ builddate }}
+{%- if package.commit %}
+commit = {{ package.commit }}
+{%- endif %}
{%- if package.data_hash %}
datahash = {{ package.data_hash }}
{%- endif %}
+{%- if package.maintainer %}
+maintainer = {{ package.maintainer }}
+{%- endif %}
""")
"""The template used for generating .PKGINFO"""
@@ -37,7 +57,9 @@ class Package:
"""The base package class."""
def __init__(self, name, version, arch, description=None, url=None, size=0,
- provides=None, depends=None, **kwargs):
+ provides=None, depends=None, license=None, origin=None,
+ replaces=None, commit=None, maintainer=None, builddate=0,
+ install_if=None, **kwargs):
"""Initialise a package object.
:param str name:
@@ -67,6 +89,30 @@ class Package:
:param list depends:
(Optional) One or more packages that are required to be installed
to use this package.
+
+ :param str license:
+ (Recommended) The license this package is under.
+
+ :param str origin:
+ (Optional) The origin package, if this package is a subpackage.
+ Defaults to `name`.
+
+ :param list replaces:
+ (Optional) One or more packages that this package replaces.
+
+ :param str commit:
+ (Recommended) The hash of the git commit the repository was on when
+ this package was built.
+
+ :param str maintainer:
+ (Recommended) The maintainer of the package.
+
+ :param int builddate:
+ (Optional) The date the package was built, in UNIX timestamp.
+ Defaults to right now.
+
+ :param list install_if:
+ (Optional) Read the APKBUILD.5 manpage.
"""
self._pkgname = name
@@ -77,6 +123,13 @@ class Package:
self._arch = arch
self._provides = provides or list()
self._depends = depends or list()
+ self._replaces = replaces or list()
+ self._iif = install_if or list()
+ self._license = license
+ self._origin = origin or name
+ self._commit = commit
+ self._maintainer = maintainer
+ self._builddate = builddate or time.time()
if '_datahash' in kwargs:
self._datahash = kwargs.pop('_datahash')
@@ -136,6 +189,36 @@ class Package:
return self._depends
@property
+ def replaces(self):
+ """The packages this package replaces."""
+ return self._replaces
+
+ @property
+ def install_if(self):
+ """The packages that pull in this package."""
+ return self._iif
+
+ @property
+ def license(self):
+ """The license of the package."""
+ return self._license
+
+ @property
+ def origin(self):
+ """The origin package of this package."""
+ return self._origin
+
+ @property
+ def commit(self):
+ """The hash of the git commit the build repository was on."""
+ return self._commit
+
+ @property
+ def maintainer(self):
+ """The maintainer of the package."""
+ return self._maintainer
+
+ @property
def data_hash(self):
"""The hash of the package's data, or None if not available."""
return getattr(self, '_datahash', None)
@@ -148,10 +231,14 @@ class Package:
def __repr__(self):
return 'Package(name="{name}", version="{ver}", arch="{arch}", '\
'description="{desc}", url="{url}", size={size}, '\
- 'provides={prov}, depends={dep})'.format(
+ 'provides={prov}, depends={dep}, license={lic}, '\
+ 'origin="{origin}", replaces={rep}, commit="{git}", '\
+ 'maintainer="{m}", builddate={ts}, install_if={iif})'.format(
name=self._pkgname, ver=self._pkgver, arch=self._arch,
desc=self._pkgdesc, prov=self._provides, dep=self._depends,
- url=self._url, size=self._size)
+ url=self._url, size=self._size, lic=self._license,
+ origin=self._origin, rep=self._replaces, git=self._commit,
+ m=self._maintainer, ts=self._builddate, iif=self._iif)
def to_pkginfo(self):
"""Serialises the package's information into the PKGINFO format.
@@ -184,11 +271,15 @@ class Package:
param_map = {'pkgname': 'name', 'pkgver': 'version', 'arch': 'arch',
'pkgdesc': 'description', 'provides': 'provides',
'depend': 'depends', 'url': 'url', 'size': 'size',
- 'datahash': '_datahash'}
- list_keys = {'provides', 'depend'}
+ 'replaces': 'replaces', 'builddate': 'builddate',
+ 'license': 'license', 'datahash': '_datahash',
+ 'maintainer': 'maintainer', 'commit': 'commit',
+ 'install_if': 'install_if'}
+ list_keys = {'provides', 'depend', 'replaces', 'install_if'}
params['provides'] = list()
params['depends'] = list()
+ params['replaces'] = list()
for line in buf.readlines():
if not isinstance(line, str):
diff --git a/setup.py b/setup.py
index 119d2e8..78d1575 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-from setuptools import setup
+from setuptools import setup, find_packages
from codecs import open
from os import path
@@ -13,8 +13,8 @@ with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
setup(
name='apkkit',
- version='0.5',
- description='Manage APK packages from Python',
+ version='0.6.0',
+ description='Manage APK packages and repositories from Python',
long_description=long_description,
url='http://adelielinux.org/',
author='A. Wilcox',
@@ -35,10 +35,11 @@ setup(
'Topic :: System :: Software Distribution',
],
keywords='apk packaging portage',
- packages=('apkkit',),
+ packages=find_packages(),
install_requires=[
'cryptography',
'jinja',
'pyyaml',
+ 'requests',
]
)