diff --git a/src/pylorax/api/bisect.py b/src/pylorax/api/bisect.py
new file mode 100644
index 00000000..a1bbdcc0
--- /dev/null
+++ b/src/pylorax/api/bisect.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+def insort_left(a, x, key=None, lo=0, hi=None):
+ """Insert item x in list a, and keep it sorted assuming a is sorted.
+
+ :param a: sorted list
+ :type a: list
+ :param x: item to insert into the list
+ :type x: object
+ :param key: Function to use to compare items in the list
+ :type key: function
+ :returns: index where the item was inserted
+ :rtype: int
+
+ If x is already in a, insert it to the left of the leftmost x.
+ Optional args lo (default 0) and hi (default len(a)) bound the
+ slice of a to be searched.
+
+ This is a modified version of bisect.insort_left that can use a
+ function for the compare, and returns the index position where it
+ was inserted.
+ """
+ if key is None:
+ key = lambda i: i
+
+ if lo < 0:
+ raise ValueError('lo must be non-negative')
+ if hi is None:
+ hi = len(a)
+ while lo < hi:
+ mid = (lo+hi)//2
+ if key(a[mid]) < key(x): lo = mid+1
+ else: hi = mid
+ a.insert(lo, x)
+ return lo
diff --git a/src/pylorax/api/projects.py b/src/pylorax/api/projects.py
index 5c31d6c0..9b31eb09 100644
--- a/src/pylorax/api/projects.py
+++ b/src/pylorax/api/projects.py
@@ -17,12 +17,14 @@
import logging
log = logging.getLogger("lorax-composer")
-import os
from configparser import ConfigParser
import dnf
from glob import glob
+import os
import time
+from pylorax.api.bisect import insort_left
+
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
@@ -75,6 +77,32 @@ def pkg_to_project(pkg):
"upstream_vcs": "UPSTREAM_VCS"}
+def pkg_to_build(pkg):
+ """Extract the build details from a hawkey.Package object
+
+ :param pkg: hawkey.Package object with package details
+ :type pkg: hawkey.Package
+ :returns: A dict with the build details, epoch, release, arch, build_time, changelog, ...
+ :rtype: dict
+
+ metadata entries are hard-coded to {}
+
+ Note that this only returns the build dict, it does not include the name, description, etc.
+ """
+ return {"epoch": pkg.epoch,
+ "release": pkg.release,
+ "arch": pkg.arch,
+ "build_time": api_time(pkg.buildtime),
+ "changelog": "CHANGELOG_NEEDED", # XXX Not in hawkey.Package
+ "build_config_ref": "BUILD_CONFIG_REF",
+ "build_env_ref": "BUILD_ENV_REF",
+ "metadata": {},
+ "source": {"license": pkg.license,
+ "version": pkg.version,
+ "source_ref": "SOURCE_REF",
+ "metadata": {}}}
+
+
def pkg_to_project_info(pkg):
"""Extract the details from a hawkey.Package object
@@ -85,25 +113,12 @@ def pkg_to_project_info(pkg):
metadata entries are hard-coded to {}
"""
- build = {"epoch": pkg.epoch,
- "release": pkg.release,
- "arch": pkg.arch,
- "build_time": api_time(pkg.buildtime),
- "changelog": "CHANGELOG_NEEDED", # XXX Not in hawkey.Package
- "build_config_ref": "BUILD_CONFIG_REF",
- "build_env_ref": "BUILD_ENV_REF",
- "metadata": {},
- "source": {"license": pkg.license,
- "version": pkg.version,
- "source_ref": "SOURCE_REF",
- "metadata": {}}}
-
return {"name": pkg.name,
"summary": pkg.summary,
"description": pkg.description,
"homepage": pkg.url,
"upstream_vcs": "UPSTREAM_VCS",
- "builds": [build]}
+ "builds": [pkg_to_build(pkg)]}
def pkg_to_dep(pkg):
@@ -180,7 +195,24 @@ def projects_info(dbo, project_names):
pkgs = dbo.sack.query().available().filter(name__glob=project_names)
else:
pkgs = dbo.sack.query().available()
- return sorted(map(pkg_to_project_info, pkgs), key=lambda p: p["name"].lower())
+
+ # iterate over pkgs
+ # - if pkg.name isn't in the results yet, add pkg_to_project_info in sorted position
+ # - if pkg.name is already in results, get its builds. If the build for pkg is different
+ # in any way (version, arch, etc.) add it to the entry's builds list. If it is the same,
+ # skip it.
+ results = []
+ results_names = {}
+ for p in pkgs:
+ if p.name.lower() not in results_names:
+ idx = insort_left(results, pkg_to_project_info(p), key=lambda p: p["name"].lower())
+ results_names[p.name.lower()] = idx
+ else:
+ build = pkg_to_build(p)
+ if build not in results[results_names[p.name.lower()]]["builds"]:
+ results[results_names[p.name.lower()]]["builds"].append(build)
+
+ return results
def _depsolve(dbo, projects, groups):
"""Add projects to a new transaction
@@ -321,9 +353,29 @@ def modules_list(dbo, module_names):
"""
# TODO - Figure out what to do with this for Fedora 'modules'
- projs = projects_info(dbo, module_names)
- return sorted(map(proj_to_module, projs), key=lambda p: p["name"].lower())
+ projs = _unique_dicts(projects_info(dbo, module_names), key=lambda p: p["name"].lower())
+ return list(map(proj_to_module, projs))
+def _unique_dicts(lst, key):
+ """Return a new list of dicts, only including one match of key(d)
+
+ :param lst: list of dicts
+ :type lst: list
+ :param key: key function to match lst entries
+ :type key: function
+ :returns: list of the unique lst entries
+ :rtype: list
+
+ Uses key(d) to test for duplicates in the returned list, creating a
+ list of unique return values.
+ """
+ result = []
+ result_keys = []
+ for d in lst:
+ if key(d) not in result_keys:
+ result.append(d)
+ result_keys.append(key(d))
+ return result
def modules_info(dbo, module_names):
"""Return details about a module, including dependencies
diff --git a/tests/pylorax/test_bisect.py b/tests/pylorax/test_bisect.py
new file mode 100644
index 00000000..8e5a101a
--- /dev/null
+++ b/tests/pylorax/test_bisect.py
@@ -0,0 +1,41 @@
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import unittest
+
+from pylorax.api.bisect import insort_left
+
+
+class BisectTest(unittest.TestCase):
+ def test_insort_left_nokey(self):
+ results = []
+ for x in range(0, 10):
+ insort_left(results, x)
+ self.assertEqual(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+ def test_insort_left_key_strings(self):
+ unsorted = ["Maggie", "Homer", "Bart", "Marge"]
+ results = []
+ for x in unsorted:
+ insort_left(results, x, key=lambda p: p.lower())
+ self.assertEqual(results, ["Bart", "Homer", "Maggie", "Marge"])
+
+ def test_insort_left_key_dict(self):
+ unsorted = [{"name":"Maggie"}, {"name":"Homer"}, {"name":"Bart"}, {"name":"Marge"}]
+ results = []
+ for x in unsorted:
+ insort_left(results, x, key=lambda p: p["name"].lower())
+ self.assertEqual(results, [{"name":"Bart"}, {"name":"Homer"}, {"name":"Maggie"}, {"name":"Marge"}])